New upstream version 11.5.3+dfsg

This commit is contained in:
Pirate Praveen 2018-12-13 13:39:08 +05:30
parent 4aecc802ff
commit e0c922f897
3195 changed files with 301857 additions and 41743 deletions

View file

@ -1,90 +1,28 @@
---
env:
browser: true
es6: true
extends: extends:
- airbnb-base - '@gitlab'
- plugin:vue/recommended
globals: globals:
__webpack_public_path__: true __webpack_public_path__: true
gl: false gl: false
gon: false gon: false
localStorage: false localStorage: false
parserOptions:
parser: babel-eslint
plugins: plugins:
- filenames
- import - import
- html - html
- promise
settings: settings:
html/html-extensions: html/html-extensions:
- ".html" - '.html'
- ".html.raw" - '.html.raw'
import/resolver: import/resolver:
webpack: webpack:
config: "./config/webpack.config.js" config: './config/webpack.config.js'
rules: rules:
filenames/match-regex:
- error
- "^[a-z0-9_]+$"
import/no-commonjs: error import/no-commonjs: error
no-multiple-empty-lines:
- error
- max: 1
promise/catch-or-return: error
no-param-reassign:
- error
- props: true
ignorePropertyModificationsFor:
- "acc" # for reduce accumulators
- "accumulator" # for reduce accumulators
- "el" # for DOM elements
- "element" # for DOM elements
- "state" # for Vuex mutations
no-underscore-dangle: no-underscore-dangle:
- error - error
- allow: - allow:
- __ - __
- _links - _links
no-mixed-operators: off
vue/html-self-closing:
- error
- html:
void: always
normal: never
component: always
svg: always
math: always
camelcase:
- error
- properties: never
ignoreDestructuring: true
## Conflicting rules with prettier:
space-before-function-paren: off
curly: off
arrow-parens: off
function-paren-newline: off
object-curly-newline: off
padded-blocks: off
# Disabled for now, to make the eslint 3 -> eslint 5 update smoother
## Indent rule. We are using the old for now: https://eslint.org/docs/user-guide/migrating-to-4.0.0#indent-rewrite
indent: off
indent-legacy:
- error
- 2
- SwitchCase: 1
VariableDeclarator: 1
outerIIFEBody: 1
FunctionDeclaration:
parameters: 1
body: 1
FunctionExpression:
parameters: 1
body: 1
# Disabled for now, to make the airbnb-base 12.1.0 -> 13.1.0 update smoother # Disabled for now, to make the airbnb-base 12.1.0 -> 13.1.0 update smoother
operator-linebreak: off
implicit-arrow-linebreak: off
no-else-return: no-else-return:
- error - error
- allowElseIf: true - allowElseIf: true

1
.gitattributes vendored
View file

@ -1 +1,2 @@
Dangerfile gitlab-language=ruby Dangerfile gitlab-language=ruby
db/schema.rb merge=merge_db_schema

1
.gitignore vendored
View file

@ -40,6 +40,7 @@ eslint-report.html
/config/redis.queues.yml /config/redis.queues.yml
/config/redis.shared_state.yml /config/redis.shared_state.yml
/config/unicorn.rb /config/unicorn.rb
/config/puma.rb
/config/secrets.yml /config/secrets.yml
/config/sidekiq.yml /config/sidekiq.yml
/config/registry.key /config/registry.key

View file

@ -1,4 +1,4 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.5-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
retry: 1 retry: 1
@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git
- gitlab-org - gitlab-org
.default-cache: &default-cache .default-cache: &default-cache
key: "ruby-2.4.4-debian-stretch-with-yarn" key: "ruby-2.4.5-debian-stretch-with-yarn"
paths: paths:
- vendor/ruby - vendor/ruby
- .yarn-cache/ - .yarn-cache/
@ -75,11 +75,6 @@ stages:
- mysql:5.7 - mysql:5.7
- redis:alpine - redis:alpine
.rails5-variables: &rails5-variables
script:
- export RAILS5=${RAILS5}
- export BUNDLE_GEMFILE=${BUNDLE_GEMFILE}
.rails5: &rails5 .rails5: &rails5
allow_failure: true allow_failure: true
only: only:
@ -139,7 +134,7 @@ stages:
- export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}" - export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}"
- apk add --update openssl - apk add --update openssl
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME
- chmod 755 $SCRIPT_NAME - chmod 755 $(basename $SCRIPT_NAME)
.rake-exec: &rake-exec .rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
@ -150,7 +145,6 @@ stages:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs-and-qa <<: *except-docs-and-qa
<<: *pull-cache <<: *pull-cache
<<: *rails5-variables
stage: test stage: test
script: script:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
@ -271,7 +265,7 @@ package-and-qa:
SCRIPT_NAME: trigger-build-docs SCRIPT_NAME: trigger-build-docs
environment: environment:
name: review-docs/$CI_COMMIT_REF_SLUG name: review-docs/$CI_COMMIT_REF_SLUG
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are CI variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693 # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup on_stop: review-docs-cleanup
@ -324,7 +318,8 @@ review-docs-cleanup:
cloud-native-image: cloud-native-image:
image: ruby:2.4-alpine image: ruby:2.4-alpine
before_script: [] before_script: []
stage: test dependencies: []
stage: post-test
allow_failure: true allow_failure: true
variables: variables:
GIT_DEPTH: "1" GIT_DEPTH: "1"
@ -594,7 +589,7 @@ static-analysis:
script: script:
- scripts/static-analysis - scripts/static-analysis
cache: cache:
key: "ruby-2.4.4-debian-stretch-with-yarn-and-rubocop" key: "ruby-2.4.5-debian-stretch-with-yarn-and-rubocop"
paths: paths:
- vendor/ruby - vendor/ruby
- .yarn-cache/ - .yarn-cache/
@ -700,7 +695,10 @@ gitlab:setup-mysql:
# Frontend-related jobs # Frontend-related jobs
gitlab:assets:compile: gitlab:assets:compile:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-git-2.18-chrome-69.0-node-8.x-yarn-1.2-graphicsmagick-1.3.29-docker-18.06.1
dependencies: [] dependencies: []
services:
- docker:stable-dind
variables: variables:
NODE_ENV: "production" NODE_ENV: "production"
RAILS_ENV: "production" RAILS_ENV: "production"
@ -709,21 +707,26 @@ gitlab:assets:compile:
WEBPACK_REPORT: "true" WEBPACK_REPORT: "true"
# we override the max_old_space_size to prevent OOM errors # we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584 NODE_OPTIONS: --max_old_space_size=3584
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://docker:2375
script: script:
- date - date
- yarn install --frozen-lockfile --production --cache-folder .yarn-cache - yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- date - date
- free -m - free -m
- bundle exec rake gitlab:assets:compile - bundle exec rake gitlab:assets:compile
- scripts/build_assets_image
artifacts: artifacts:
name: webpack-report name: webpack-report
expire_in: 31d expire_in: 31d
paths: paths:
- webpack-report/ - webpack-report/
- public/assets/ - public/assets/
tags:
- docker
karma: karma:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-pull-cache-job
<<: *use-pg <<: *use-pg
dependencies: dependencies:
- compile-assets - compile-assets
@ -929,3 +932,93 @@ no_ee_check:
- scripts/no-ee-check - scripts/no-ee-check
only: only:
- //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ce
# GitLab Review apps
review:
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
stage: test
allow_failure: true
before_script:
- gem install gitlab --no-document
variables:
GIT_DEPTH: "1"
HOST_SUFFIX: "$CI_ENVIRONMENT_SLUG"
DOMAIN: "-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN"
GITLAB_HELM_CHART_REF: "master"
script:
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
- export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
- export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
- source ./scripts/review_apps/review-apps.sh
- BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
- check_kube_domain
- download_gitlab_chart
- ensure_namespace
- install_tiller
- create_secret
- install_external_dns
- deploy
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://gitlab-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN
on_stop: stop_review
only:
refs:
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
kubernetes: active
except:
refs:
- master
- /(^docs[\/-].*|.*-docs$)/
stop_review:
<<: *single-script-job
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
stage: test
allow_failure: true
cache: {}
dependencies: []
variables:
SCRIPT_NAME: "review_apps/review-apps.sh"
script:
- source $(basename "${SCRIPT_NAME}")
- delete
- cleanup
when: manual
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
only:
refs:
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
kubernetes: active
except:
- master
- /(^docs[\/-].*|.*-docs$)/
schedule:review_apps_cleanup:
<<: *dedicated-no-docs-pull-cache-job
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
stage: build
allow_failure: true
cache: {}
dependencies: []
before_script:
- gem install gitlab --no-document
variables:
GIT_DEPTH: "1"
script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
environment:
name: review/auto-cleanup
action: stop
only:
refs:
- schedules@gitlab-org/gitlab-ce
- schedules@gitlab-org/gitlab-ee
kubernetes: active
except:
- tags
- /(^docs[\/-].*|.*-docs$)/

View file

@ -16,7 +16,6 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add a link to the MR to the [links section](#links) - [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required - [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**. - [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
#### Backports #### Backports
@ -26,7 +25,8 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable) - [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- [ ] Create each MR targetting the security branch `security-X-Y` - [ ] Create each MR targetting the security branch `security-X-Y`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager. - [ ] Add the ~"Merge into Security" label to all of the MRs.
- [ ] Make sure all MRs have a link in the [links section](#links)
[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script [secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script

View file

@ -38,22 +38,22 @@ test plan](https://testing.googleblog.com/2011/09/10-minute-test-plan.html) and
[this wiki page from an open-source tool that implements the ACC [this wiki page from an open-source tool that implements the ACC
model](https://code.google.com/archive/p/test-analytics/wikis/AccExplained.wiki). --> model](https://code.google.com/archive/p/test-analytics/wikis/AccExplained.wiki). -->
| | Simple | Secure | Responsive | Obvious | Stable | | | Secure | Responsive | Intuitive | Reliable |
|------------|:------:|:------:|:----------:|:-------:|:------:| |------------|:------:|:----------:|:---------:|:--------:|
| Admin | | | | | | | Admin | | | | |
| Groups | | | | | | | Groups | | | | |
| Project | | | | | | | Project | | | | |
| Repository | | | | | | | Repository | | | | |
| Issues | | | | | | | Issues | | | | |
| MRs | | | | | | | MRs | | | | |
| CI/CD | | | | | | | CI/CD | | | | |
| Ops | | | | | | | Ops | | | | |
| Registry | | | | | | | Registry | | | | |
| Wiki | | | | | | | Wiki | | | | |
| Snippets | | | | | | | Snippets | | | | |
| Settings | | | | | | | Settings | | | | |
| Tracking | | | | | | | Tracking | | | | |
| API | | | | | | | API | | | | |
## Capabilities ## Capabilities
@ -65,7 +65,7 @@ more complex features could involve multiple or even all.
Example (from https://gitlab.com/gitlab-org/gitlab-ce/issues/50353): Example (from https://gitlab.com/gitlab-org/gitlab-ce/issues/50353):
* Respository is * Respository is
* Simple * Intuitive
* It's easy to select the desired file template * It's easy to select the desired file template
* It doesn't require unnecessary actions to save the change * It doesn't require unnecessary actions to save the change
* It's easy to undo the change after selecting a template * It's easy to undo the change after selecting a template
@ -93,4 +93,4 @@ When adding new automated tests, please keep [testing levels](https://docs.gitla
in mind. in mind.
--> -->
/label ~Quality /label ~Quality ~"test plan"

View file

@ -1,8 +1,23 @@
Add a description of your merge request here. Merge requests without an adequate ## What does this MR do?
description will not be reviewed until one is added.
<!--
Describe in detail what your merge request does, why it does that, etc. Merge
requests without an adequate description will not be reviewed until one is
added.
Please also keep this description up-to-date with any discussion that takes
place so that reviewers can understand your intent. This is especially
important if they didn't participate in the discussion.
Make sure to remove this comment when you are done.
-->
Add a description of your merge request here.
## Database checklist ## Database checklist
- [ ] Conforms to the [database guides](https://docs.gitlab.com/ee/development/README.html#databases-guides)
When adding migrations: When adding migrations:
- [ ] Updated `db/schema.rb` - [ ] Updated `db/schema.rb`
@ -35,16 +50,9 @@ When removing columns, tables, indexes or other structures:
- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary - [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/documentation/index.html#contributing-to-docs) - [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/documentation/index.html#contributing-to-docs)
- [ ] [API support added](https://docs.gitlab.com/ee/development/api_styleguide.html)
- [ ] [Tests added for this feature/bug](https://docs.gitlab.com/ee/development/testing_guide/index.html) - [ ] [Tests added for this feature/bug](https://docs.gitlab.com/ee/development/testing_guide/index.html)
- Conforms to the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html) - [ ] Conforms to the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
- [ ] Has been reviewed by a Backend [maintainer](https://about.gitlab.com/handbook/engineering/#maintainer)
- [ ] Has been reviewed by a Database [specialist](https://about.gitlab.com/team/structure/#specialist)
- [ ] Conforms to the [merge request performance guidelines](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html) - [ ] Conforms to the [merge request performance guidelines](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
- [ ] Conforms to the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides) - [ ] Conforms to the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
- [ ] If you have multiple commits, please combine them into a few logically organized commits by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
- [ ] [Internationalization required/considered](https://docs.gitlab.com/ee/development/i18n/index.html)
- [ ] For a paid feature, have we considered GitLab.com plans, how it works for groups, and is there a design for promoting it to users who aren't on the correct plan?
- [ ] [End-to-end tests](https://docs.gitlab.com/ee/development/testing_guide/end_to_end_tests.html#testing-code-in-merge-requests) pass (`package-and-qa` manual pipeline job)
/label ~database /label ~database

View file

@ -19,6 +19,7 @@ Closes
- [ ] [Apply the correct labels and milestone](https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process) - [ ] [Apply the correct labels and milestone](https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process)
- [ ] Crosslink the document from the higher-level index - [ ] Crosslink the document from the higher-level index
- [ ] Crosslink the document from other subject-related docs - [ ] Crosslink the document from other subject-related docs
- [ ] Feature moving tiers? Make sure the change is also reflected in [`features.yml`](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml)
- [ ] Correctly apply the product [badges](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges) and [tiers](https://docs.gitlab.com/ee/development/documentation/styleguide.html#gitlab-versions-and-tiers) - [ ] Correctly apply the product [badges](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges) and [tiers](https://docs.gitlab.com/ee/development/documentation/styleguide.html#gitlab-versions-and-tiers)
- [ ] [Port the MR to EE (or backport from CE)](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee): _always recommended, required when the `ee-compat-check` job fails_ - [ ] [Port the MR to EE (or backport from CE)](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee): _always recommended, required when the `ee-compat-check` job fails_

View file

@ -3,3 +3,7 @@
/public/ /public/
/vendor/ /vendor/
/tmp/ /tmp/
# ignore stylesheets for now as this clashes with our linter
*.css
*.scss

View file

@ -3,7 +3,9 @@ inherit_gem:
- rubocop-default.yml - rubocop-default.yml
inherit_from: .rubocop_todo.yml inherit_from: .rubocop_todo.yml
require: ./rubocop/rubocop require:
- ./rubocop/rubocop
- rubocop-rspec
AllCops: AllCops:
TargetRailsVersion: 4.2 TargetRailsVersion: 4.2
@ -48,12 +50,20 @@ Style/FrozenStringLiteralComment:
- 'danger/**/*' - 'danger/**/*'
- 'db/**/*' - 'db/**/*'
- 'ee/**/*' - 'ee/**/*'
- 'lib/**/*' - 'lib/gitlab/**/*'
- 'lib/tasks/**/*'
- 'qa/**/*' - 'qa/**/*'
- 'rubocop/**/*' - 'rubocop/**/*'
- 'scripts/**/*' - 'scripts/**/*'
- 'spec/**/*' - 'spec/**/*'
RSpec/FilePath:
Exclude:
- 'qa/**/*'
- 'spec/javascripts/fixtures/*'
- 'ee/spec/javascripts/fixtures/*'
- 'spec/requests/api/v3/*'
Naming/FileName: Naming/FileName:
ExpectMatchingDefinition: true ExpectMatchingDefinition: true
Exclude: Exclude:
@ -66,15 +76,20 @@ Naming/FileName:
- 'qa/qa/specs/**/*' - 'qa/qa/specs/**/*'
- 'qa/bin/*' - 'qa/bin/*'
- 'config/**/*' - 'config/**/*'
- 'ee/config/**/*'
- 'lib/generators/**/*' - 'lib/generators/**/*'
- 'locale/unfound_translations.rb' - 'locale/unfound_translations.rb'
- 'ee/locale/unfound_translations.rb' - 'ee/locale/unfound_translations.rb'
- 'ee/lib/generators/**/*' - 'ee/lib/generators/**/*'
- 'qa/qa/scenario/test/integration/ldap_no_tls.rb'
- 'qa/qa/scenario/test/integration/ldap_tls.rb'
IgnoreExecutableScripts: true IgnoreExecutableScripts: true
AllowedAcronyms: AllowedAcronyms:
- EE - EE
- JSON - JSON
- LDAP - LDAP
- SAML
- IO - IO
- HMAC - HMAC
- QA - QA

View file

@ -1 +1 @@
2.4.4 2.4.5

View file

@ -2,53 +2,312 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.4.9 (2018-12-03) ## 11.5.3 (2018-12-06)
### Fixed (2 changes) ### Security (1 change)
- Prevent a path traversal attack on global file templates.
## 11.5.2 (2018-12-03)
### Removed (1 change)
- Removed Site Statistics optimization as it was causing problems. !23314
### Fixed (6 changes, 1 of them is from the community)
- Display impersonation token value only after creation. !22916 - Display impersonation token value only after creation. !22916
- Fix not render emoji in filter dropdown. !23112 (Hiroyuki Sato)
- Fixes stuck tooltip on stop env button. !23244
- Correctly handle data-loss scenarios when encrypting columns. !23306 - Correctly handle data-loss scenarios when encrypting columns. !23306
- Clear BatchLoader context between Sidekiq jobs. !23308
- Fix handling of filenames with hash characters in tree view. !23368
## 11.4.8 (2018-11-27) ## 11.5.1 (2018-11-26)
### Security (24 changes) ### Security (17 changes)
- Escape entity title while autocomplete template rendering to prevent XSS. !2571
- Resolve reflected XSS in Ouath authorize window.
- Fix XSS in merge request source branch name.
- Escape user fullname while rendering autocomplete template to prevent XSS. - Escape user fullname while rendering autocomplete template to prevent XSS.
- Fix CRLF vulnerability in Project hooks. - Fix CRLF vulnerability in Project hooks.
- Fix possible XSS attack in Markdown urls with spaces. - Fix possible XSS attack in Markdown urls with spaces.
- Redact sensitive information on gitlab-workhorse log. - Redact sensitive information on gitlab-workhorse log.
- Do not follow redirects in Prometheus service when making http requests to the configured api url. - Do not follow redirects in Prometheus service when making http requests to the configured api url.
- Persist only SHA digest of PersonalAccessToken#token.
- Don't expose confidential information in commit message list. - Don't expose confidential information in commit message list.
- Provide email notification when a user changes their email address. - Provide email notification when a user changes their email address.
- Restrict Personal Access Tokens to API scope on web requests. - Restrict Personal Access Tokens to API scope on web requests.
- Redact personal tokens in unsubscribe links. - Resolve reflected XSS in Ouath authorize window.
- Fix SSRF in project integrations. - Fix SSRF in project integrations.
- Fixed ability to comment on locked/confidential issues. - Fixed ability to comment on locked/confidential issues.
- Fixed ability of guest users to edit/delete comments on locked or confidential issues. - Fixed ability of guest users to edit/delete comments on locked or confidential issues.
- Fix milestone promotion authorization check. - Fix milestone promotion authorization check.
- Monkey kubeclient to not follow any redirects.
- Configure mermaid to not render HTML content in diagrams. - Configure mermaid to not render HTML content in diagrams.
- Fix a possible symlink time of check to time of use race condition in GitLab Pages. - Fix a possible symlink time of check to time of use race condition in GitLab Pages.
- Removed ability to see private group names when the group id is entered in the url. - Removed ability to see private group names when the group id is entered in the url.
- Fix stored XSS for Environments. - Fix stored XSS for Environments.
- Prevent SSRF attacks in HipChat integration.
- Validate Wiki attachments are valid temporary files.
## 11.4.7 (2018-11-20) ## 11.5.0 (2018-11-22)
- No changes. ### Security (10 changes, 1 of them is from the community)
## 11.4.6 (2018-11-18)
### Security (1 change)
- Escape entity title while autocomplete template rendering to prevent XSS. !2556
- Update moment to 2.22.2. !22648 (Takuya Noguchi)
- Redact personal tokens in unsubscribe links.
- Escape user fullname while rendering autocomplete template to prevent XSS. - Escape user fullname while rendering autocomplete template to prevent XSS.
- Persist only SHA digest of PersonalAccessToken#token.
- Monkey kubeclient to not follow any redirects.
- Prevent SSRF attacks in HipChat integration.
- Prevent templated services from being imported.
- Validate Wiki attachments are valid temporary files.
- Fix XSS in merge request source branch name.
### Removed (2 changes)
- Remove Git circuit breaker. !22212
- Remove Koding integration and documentation. !22334
### Fixed (74 changes, 15 of them are from the community)
- Hide all tables on Pipeline when no Jobs for the Pipeline. !18540 (Takuya Noguchi)
- Fixing count on Milestones. !21446
- Use case insensitve username lookups. !21728 (William George)
- Correctly process Bamboo API result array. !21970 (Alex Lossent)
- Fix 'merged with' UI being displayed when merge request has no merge commit. !22022
- Fix broken file name navigation on MRs. !22109
- Fix incorrect spacing between buttons when commenting on a MR. !22135
- Vertical align Pipeline Graph in Commit Page. !22173 (Johann Hubert Sonntagbauer)
- Reject invalid branch names in repository compare controller. !22186
- Fix size of emojis of user status in user menu. !22194
- Use the standard PIP_CACHE_DIR for Python dependency caching template. !22211 (Takuya Noguchi)
- Fix bug with wiki attachments content disposition. !22220
- Does not allow a SSH URI when importing new projects. !22309
- fix duplicated key in license management job auto devops gitlab ci template. !22311 (Adam Lemanski)
- Fix commit signature error when project is disabled. !22344
- Show available clusters when installed or updated. !22356
- Fix auto-corrected upload URLs in webhooks. !22361
- Fix a bug displaying certain wiki pages. !22377
- Fix prometheus graphs in firefox. !22400
- Resolve assign-me quick action doesn't work if there is extra white space. !22402
- Remove base64 encoding from files that contain plain text. !22425
- Strip whitespace around GitHub personal access tokens. !22432
- Fix 500 error when testing webhooks with redirect loops. !22447 (Heinrich Lee Yu)
- Fix rendering of 'Protected' value on Runner details page. !22459
- Fix bug stopping non-admin users from changing visibility level on group creation. !22468
- Make Issue Board sidebar show project-specific labels based on selected Issue. !22475
- Fix EOF detection with CI artifacts metadata. !22479
- Fix transient spec error in the bar_chart component. !22495
- Resolve LFS not correctly showing enabled. !22501
- If user was not found, service hooks won't run on post receive background job. !22519
- Fix broken "Show whitespace changes" button on MRs. !22539
- Always show new issue button in boards' Open list. !22557 (Heinrich Lee Yu)
- Add transparent background to markdown header tabs. !22565 (George Tsiolis)
- Use gitlab_environment for ldap rake task. !22582
- Add commit message to commit tree anchor title. !22585
- Cache pipeline status per SHA. !22589
- Change HELM_HOST in Auto-DevOps template to work behind proxy. !22596 (Sergej Nikolaev <kinolaev@gmail.com>)
- Show user status for label events in system notes. !22609
- Fix extra merge request versions created from forked merge requests. !22611
- Remove PersonalAccessTokensFinder#find_by method. !22617
- Fix search "all in GitLab" not working with relative URLs. !22644
- Fix quick links button styles. !22657 (George Tsiolis)
- Fix #53298: JupyterHub restarts should work without errors. !22671 (Amit Rathi)
- Fix incompatibility with IE11 due to non-transpiled gitlab-ui components. !22695
- Fix bug when links in tabs of the labels index pages ends with .html. !22716
- Fixed label removal from issue. !22762
- Align toggle sidebar button across all browsers and OSs. !22771
- Disable replication lag check for Aurora PostgreSQL databases. !22786
- Render unescaped link for failed pipeline status. !22807
- Fix misaligned approvers dropdown. !22832
- Fix bug with wiki page create message. !22849
- Fix rendering of filter bar tokens for special values. !22865 (Heinrich Lee Yu)
- Align sign in button. !22888 (George Tsiolis)
- Fix error handling bugs in kubernetes integration. !22922
- Fix deployment jobs using nil KUBE_TOKEN due to migration issue. !23009
- Avoid returning deployment metrics url to MR widget when the deployment is not successful. !23010
- Fix a race condition intermittently breaking GitLab startup. !23028
- Adds margin after a deleted branch name in the activity feed. !23038
- Ignore environment validation failure. !23100
- Fixes broken borders for reports section in MR widget.
- Adds CI favicon back to jobs page.
- Redirect to the pipeline builds page when a build is canceled. (Eva Kadlecova)
- Fixed diff stats not showing when performance bar is enabled.
- Show expand all diffs button when a single diff file is collapsed.
- Clear fetched file templates when changing template type in Web IDE.
- Fix bug causing not all emails to show up in commit email selectbox.
- Remove duplicate escape in job sidebar.
- Fixing styling issues on the scheduled pipelines page.
- Renders stuck block when runners are stuck.
- Removes extra border from test reports in the merge request widget.
- Only render link to branch when branch still exists in pipeline page.
- Fixed source project not filtering in merge request creation compare form.
- Do not reload self on hooks when creating deployment.
- Fixes broken test in master.
### Changed (38 changes, 12 of them are from the community)
- Link button in markdown editor recognize URLs. !1983 (Johann Hubert Sonntagbauer)
- Replace i to icons in vue components. !20748 (George Tsiolis)
- Remove Linguist gem, reducing Rails memory usage by 128MB per process. !21008
- Issue board card design. !21229
- On deletion of a file in sub directory in web IDE redirect to the sub directory instead of project root. !21465 (George Thomas @thegeorgeous)
- Change single-item breadcrumbs to page titles. !22155
- Improving branch filter sorting by listing exact matches first and added support for begins_with (^) and ends_with ($) matching. !22166 (Jason Rutherford)
- Remove legacy unencrypted webhook columns from the database. !22199
- Show canary status in the performance bar. !22222
- Add failure reason for execution timeout. !22224
- Rename "scheduled" label/badge of delayed jobs to "delayed". !22245
- Update the empty state on wiki-only projects to display an empty state that is more consistent with the rest of the system. !22262
- Add IID headers to E-Mail notifications. !22263
- Allow finding the common ancestor for multiple revisions through the API. !22295
- Add status to Deployment. !22380
- Add dynamic timer to delayed jobs. !22382
- No longer require a deploy to start Prometheus monitoring. !22401
- Secret Variables renamed to CI Variables in the codebase, to match UX. !22414 (Marcel Amirault @ravlen)
- Automatically navigate to last board visited. !22430
- Use merge request prefix symbol in event feed title. !22449 (George Tsiolis)
- Update Ruby version in README. !22466 (J.D. Bean)
- Reword error message for internal CI unknown pipeline status. !22474
- Bump mermaid to 8.0.0-rc.8. !22509 (@blackst0ne)
- Update Todo icons in collapsed sidebar for Issues and MRs. !22534
- Support backward compatibility when introduce new failure reason. !22566
- Add dynamic timer for delayed jobs in pipelines list. !22621
- Truncate milestone title on collapsed sidebar. !22624 (George Tsiolis)
- Standardize milestones filter in APIs to None / Any. !22637 (Heinrich Lee Yu)
- Add dynamic timer for delayed jobs in job list. !22656
- Allowing issues with single letter identifiers to be linked to external issue tracker (f.ex T-123). !22717 (Dídac Rodríguez Arbonès)
- Update project and group labels empty state. !22745 (George Tsiolis)
- Fix environment status in merge request widget. !22799
- Paginate Bitbucket Server importer projects. !22825
- Drop `allow_overflow` option in `TimeHelper.duration_in_numbers`. !52284
- Add 'only history' option to notes filter.
- Adds filtered dropdown with changed files in review.
- Expose {closed,merged}_{at,by} in merge requests API index.
- Make all legacy security reports to use raw format.
### Performance (27 changes, 6 of them are from the community)
- Add preload for routes and namespaces for issues controller. !21651
- Enhance performance of counting local LFS objects. !22143
- Use cached readme contents when available. !22325
- Experimental support for running Puma multithreaded web-server. !22372
- Enhance performance of counting local Uploads. !22522
- Reduce SQL queries needed to load open merge requests. !22709
- Significantly cut memory usage and SQL queries when reloading diffs. !22725
- Optimize merge request refresh by using the database to check commit SHAs. !22731
- Remove dind from license_management auto-devops job definition. !22732
- Add index to find stuck merge requests. !22749
- Allow Rails concurrency when running in Puma. !22751
- Improve performance of rendering large reports. !22835
- Improves performance of stuck import jobs detection. !22879
- Rewrite SnippetsFinder to improve performance by a factor of 1500.
- Enable more frozen string in lib/**/*.rb. (gfyoung)
- Enable some frozen string in lib/gitlab. (gfyoung)
- Enable even more frozen string in lib/**/*.rb. (gfyoung)
- Improve performance of tree rendering in repositories with lots of items.
- Remove gitlab-ui's tooltip from global.
- Remove gitlab-ui's progress bar from global.
- Remove gitlab-ui's pagination from global.
- Remove gitlab-ui's modal from global.
- Remove gitlab-ui's loading icon from global.
- Enable frozen string for lib/gitlab/*.rb. (gfyoung)
- Enable frozen string for lib/gitlab/ci. (gfyoung)
- Enable frozen string for remaining lib/gitlab/ci/**/*.rb. (gfyoung)
- Adds pagination to pipelines table in merge request page.
### Added (33 changes, 11 of them are from the community)
- Add endpoint to update a git submodule reference. !20949
- Add license data to projects endpoint. !21606 (J.D. Bean (@jdbean))
- Allow to configure when to retry failed CI jobs. !21758 (Markus Doits)
- Add API endpoint to list issue related merge requests. !21806 (Helmut Januschka)
- Add the Play button for delayed jobs in environment page. !22106
- Switch between tree list & file list in diffs file browser. !22191
- Re-arrange help-related user menu items into new Help menu. !22195
- Adds trace of each access check when git push times out. !22265
- Add email for milestone change. !22279
- Show post-merge pipeline in merge request page. !22292
- Add Applications API endpoints for listing and deleting entries. !22296 (Jean-Baptiste Vasseur)
- Added `Any` option to milestones filter. !22351 (Heinrich Lee Yu)
- Improve validation errors for external CI/CD configuration. !22394
- Introduce new model to persist specific cluster information. !22404
- Add background migration to populate Kubernetes namespaces. !22433
- Add support for JSON logging for audit events. !22471
- Adds option to override commit email with a noreply private email. !22560
- Add None/Any option for assignee_id in Issues and Merge Requests API. !22598 (Heinrich Lee Yu)
- Add None/Any option for assignee_id in search bar. !22599 (Heinrich Lee Yu)
- Implement parallel job keyword. !22631
- Add None / Any options to reactions filter. !22638 (Heinrich Lee Yu)
- Make index.* render like README.* when it's present in a repository. !22639 (Jakub Jirutka)
- Allow adding patches when creating a merge request via email. !22723 (Serdar Dogruyol)
- Bump Gitaly to 0.129.0. !22868
- Allow commenting on any diff line in Merge Requests. !22914
- Add revert to commits API. !22919
- Introduce Knative support. !43959 (Chris Baumbauer)
- Reimplemented image commenting in merge request diffs.
- Soft-archive old jobs.
- Renders warning info when job is archieved.
- Support licenses and performance.
- Filter notes by comments or activity for issues and merge requests.
- Bump Gitaly to 0.128.0.
### Other (54 changes, 18 of them are from the community)
- Remove .card-title from .card-header for BS4 migration. !19335 (Takuya Noguchi)
- Update group settings/edit page to new design. !21115
- Change markdown header tab anchor links to buttons. !21988 (George Tsiolis)
- Replace tooltip in markdown component with gl-tooltip. !21989 (George Tsiolis)
- Extend RBAC by having a service account restricted to project's namespace. !22011
- Update images in group docs. !22031 (Marc Schwede)
- Add gitlab:gitaly:check task for Gitaly health check. !22063
- Add new sort option "most_stars" to "Group > Children" pages. !22121 (Rene Hennig)
- Fix inaccessible dropdown for code-less projects. !22137
- Rails5: fix user edit profile clear status spec. !22169 (Jasper Maes)
- Rails 5: fix mysql milliseconds problems in scheduled build specs. !22170 (Jasper Maes)
- Focus project slug on tab navigation. !22198
- Redesign activity feed. !22217
- Update used version of Runner Helm Chart to 0.1.34. !22274
- Update environments empty state. !22297 (George Tsiolis)
- Adds model and migrations to enable group level clusters. !22307
- Use literal instead of constructor for creating regex. !22367
- Remove prometheus configuration help text. !22413 (George Tsiolis)
- Rails5: fix deployment model spec. !22428 (Jasper Maes)
- Change to top level controller for clusters so that we can use it for project clusters (now) and group clusters (later). !22438
- Remove empty spec describe blocks. !22451 (George Tsiolis)
- Change branch font type in tag creation. !22454 (George Tsiolis)
- Rails5: fix delete blob. !22456 (Jasper Maes)
- Start tracking shards and pool repositories in the database. !22482
- Allow kubeclient to call RoleBinding methods. !22524
- Introduce new kubernetes helpers. !22525
- Adds container to pager to enable scoping. !22529
- Update used version of Runner Helm Chart to 0.1.35. !22541
- Removes experimental labels from cluster views. !22550
- Combine all datetime library functions into 'datetime_utility.js'. !22570
- Upgrade Prometheus to 2.4.3 and Alertmanager to 0.15.2. !22600
- Fix stage dropdown not rendering in different languages. !22604
- Remove asset_sync gem from Gemfile and related code from codebase. !22610
- Use key-value pair arrays for API query parameter logging instead of hashes. !22623
- Replace deprecated uniq on a Relation with distinct. !22625 (Jasper Maes)
- Remove mousetrap-rails gem. !22647 (Takuya Noguchi)
- Fix IDE typos in props. !22685 (George Tsiolis)
- Add scheduled flag to job entity. !22710
- Remove `ci_enable_scheduled_build` feature flag. !22742
- Add endpoints for simulating certain failure modes in the application. !22746
- Bump KUBERNETES_VERSION for Auto DevOps to latest 1.10 series. !22757
- Fix statement timeouts in RemoveRestrictedTodos migration. !22795
- Rails5: fix mysql milliseconds issue in deployment model specs. !22850 (Jasper Maes)
- Update GitLab-Workhorse to v7.1.0. !22883
- Update JIRA service UI to accept email and API token.
- Update wiki empty state. (George Tsiolis)
- Only renders dropdown for review app changes when we have a list of files to show. Otherwise will render the regular review app button.
- Associate Rakefile with Ruby icon in diffs.
- Uses gitlab-ui components in jobs components.
- Create new group: Rename form fields and update UI.
- Transform job page into a single Vue+Vuex application.
- Updates svg dependency.
- Adds missing i18n to pipelines table.
- Disables stop environment button while the deploy is in progress.
## 11.4.5 (2018-11-04) ## 11.4.5 (2018-11-04)
@ -320,6 +579,41 @@ entry.
- Check frozen string in style builds. (gfyoung) - Check frozen string in style builds. (gfyoung)
## 11.3.9 (2018-10-31)
### Security (1 change)
- Monkey kubeclient to not follow any redirects.
## 11.3.8 (2018-10-27)
- No changes.
## 11.3.7 (2018-10-26)
### Security (6 changes)
- Escape entity title while autocomplete template rendering to prevent XSS. !2557
- Persist only SHA digest of PersonalAccessToken#token.
- Fix XSS in merge request source branch name.
- Redact personal tokens in unsubscribe links.
- Prevent SSRF attacks in HipChat integration.
- Validate Wiki attachments are valid temporary files.
## 11.3.6 (2018-10-17)
- No changes.
## 11.3.5 (2018-10-15)
### Fixed (2 changes)
- Fix loading issue on some merge request discussion. !21982
- Fix project deletion when there is a export available. !22276
## 11.3.3 (2018-10-04) ## 11.3.3 (2018-10-04)
- No changes. - No changes.
@ -597,6 +891,28 @@ entry.
- Creates Vue component for artifacts block on job page. - Creates Vue component for artifacts block on job page.
## 11.2.8 (2018-10-31)
### Security (1 change)
- Monkey kubeclient to not follow any redirects.
## 11.2.7 (2018-10-27)
- No changes.
## 11.2.6 (2018-10-26)
### Security (5 changes)
- Escape entity title while autocomplete template rendering to prevent XSS. !2558
- Fix XSS in merge request source branch name.
- Redact personal tokens in unsubscribe links.
- Persist only SHA digest of PersonalAccessToken#token.
- Prevent SSRF attacks in HipChat integration.
## 11.2.5 (2018-10-05) ## 11.2.5 (2018-10-05)
### Security (3 changes) ### Security (3 changes)

View file

@ -64,184 +64,56 @@ As of July 2018, all the documentation for contributing to the GitLab project ha
## Contribute to GitLab ## Contribute to GitLab
Thank you for your interest in contributing to GitLab. This guide details how This [documentation](doc/development/contributing/index.md#contribute-to-gitlab) has been moved.
to contribute to GitLab in a way that is easy for everyone.
For a first-time step-by-step guide to the contribution process, please see
["Contributing to GitLab"](https://about.gitlab.com/contributing/).
Looking for something to work on? Look for issues in the [Backlog (Accepting merge requests) milestone](#i-want-to-contribute).
GitLab comes in two flavors, GitLab Community Edition (CE) our free and open
source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for
abbreviation.
To get an overview of GitLab community membership including those that would be reviewing or merging your contributions, please visit [the community roles page](doc/development/contributing/community_roles.md).
If you want to know how the GitLab [core team]
operates please see [the GitLab contributing process](PROCESS.md).
[GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
## Security vulnerability disclosure ## Security vulnerability disclosure
Please report suspected security vulnerabilities in private to This [documentation](doc/development/contributing/index.md#security-vulnerability-disclosure) has been moved.
`support@gitlab.com`, also see the
[disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/).
Please do **NOT** create publicly viewable issues for suspected security
vulnerabilities.
## Code of conduct ## Code of Conduct
As contributors and maintainers of this project, we pledge to respect all This [documentation](https://about.gitlab.com/contributing/code-of-conduct/) has been moved.
people who contribute through reporting issues, posting feature requests,
updating documentation, submitting pull requests or patches, and other
activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual
language or imagery, derogatory comments or personal attacks, trolling, public
or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct. Project maintainers who do not
follow the Code of Conduct may be removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior can be
reported by emailing `contact@gitlab.com`.
This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
## Closing policy for issues and merge requests ## Closing policy for issues and merge requests
GitLab is a popular open source project and the capacity to deal with issues This [documentation](doc/development/contributing/index.md#closing-policy-for-issues-and-merge-requests) has been moved.
and merge requests is limited. Out of respect for our volunteers, issues and
merge requests not in line with the guidelines listed in this document may be
closed without notice.
Please treat our volunteers with courtesy and respect, it will go a long way
towards getting your issue resolved.
Issues and merge requests should be in English and contain appropriate language
for audiences of all ages.
If a contributor is no longer actively working on a submitted merge request
we can decide that the merge request will be finished by one of our
[Merge request coaches][team] or close the merge request. We make this decision
based on how important the change is for our product vision. If a Merge request
coach is going to finish the merge request we assign the
~"coach will finish" label.
## Helping others ## Helping others
Please help other GitLab users when you can. This [documentation](doc/development/contributing/index.md#helping-others) has been moved.
The methods people will use to seek help can be found on the [getting help page][getting-help].
Sign up for the mailing list, answer GitLab questions on StackOverflow or
respond in the IRC channel. You can also sign up on [CodeTriage][codetriage] to help with
the remaining issues on the GitHub issue tracker.
## I want to contribute! ## I want to contribute!
If you want to contribute to GitLab, [issues in the Backlog (Accepting merge requests)](https://gitlab.com/gitlab-org/gitlab-ce/issues?scope=all&utf8=✓&state=opened&assignee_id=0&milestone_title=Backlog%20&#40;Accepting%20merge%20requests&#41;) This [documentation](doc/development/contributing/index.md#i-want-to-contribute) has been moved.
are a great place to start. Issues with a lower weight (1 or 2) are deemed
suitable for beginners. These issues will be of reasonable size and challenge,
for anyone to start contributing to GitLab. If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to
learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel
please consider we favor
[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution!
## Contribution Flow ## Contribution Flow
When contributing to GitLab, your merge request is subject to review by merge request maintainers of a particular specialty. This [documentation](doc/development/contributing/index.md) has been moved.
When you submit code to GitLab, we really want it to get merged, but there will be times when it will not be merged.
When maintainers are reading through a merge request they may request guidance from other maintainers. If merge request maintainers conclude that the code should not be merged, our reasons will be fully disclosed. If it has been decided that the code quality is not up to GitLabs standards, the merge request maintainer will refer the author to our docs and code style guides, and provide some guidance.
Sometimes style guides will be followed but the code will lack structural integrity, or the maintainer will have reservations about the codes overall quality. When there is a reservation the maintainer will inform the author and provide some guidance. The author may then choose to update the merge request. Once the merge request has been updated and reassigned to the maintainer, they will review the code again. Once the code has been resubmitted any number of times, the maintainer may choose to close the merge request with a summary of why it will not be merged, as well as some guidance. If the merge request is closed the maintainer will be open to discussion as to how to improve the code so it can be approved in the future.
GitLab will do its best to review community contributions as quickly as possible. Specially appointed developers review community contributions daily. You may take a look at the [team page](https://about.gitlab.com/team/) for the merge request coach who specializes in the type of code you have written and mention them in the merge request. For example, if you have written some JavaScript in your code then you should mention the frontend merge request coach. If your code has multiple disciplines you may mention multiple merge request coaches.
GitLab receives a lot of community contributions, so if your code has not been reviewed within 4 days of its initial submission feel free to re-mention the appropriate merge request coach.
When submitting code to GitLab, you may feel that your contribution requires the aid of an external library. If your code includes an external library please provide a link to the library, as well as reasons for including it.
When your code contains more than 500 changes, any major breaking changes, or an external library, `@mention` a maintainer in the merge request. If you are not sure who to mention, the reviewer will add one early in the merge request process.
[core team]: https://about.gitlab.com/core-team/
[team]: https://about.gitlab.com/team/
[getting-help]: https://about.gitlab.com/getting-help/
[codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[accepting-mrs-weight]: https://gitlab.com/gitlab-org/gitlab-ce/issues?assignee_id=0&label_name[]=Accepting%20Merge%20Requests&sort=weight_asc
[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues
[ee-tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues
[google-group]: https://groups.google.com/forum/#!forum/gitlabhq
[stackoverflow]: https://stackoverflow.com/questions/tagged/gitlab
[fpl]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=feature+proposal
[accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests
[accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests
[gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
[definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html
[contributor-covenant]: http://contributor-covenant.org
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[changelog]: doc/development/changelog.md "Generate a changelog entry"
[doc-guidelines]: doc/development/documentation/index.md "Documentation guidelines"
[js-styleguide]: doc/development/fe_guide/style_guide_js.md "JavaScript styleguide"
[scss-styleguide]: doc/development/fe_guide/style_guide_scss.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[testing]: doc/development/testing_guide/index.md
[us-english]: https://en.wikipedia.org/wiki/American_English
## Workflow labels ## Workflow labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Type labels ### Type labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Subject labels ### Subject labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Team labels ### Team labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Release Scoping labels ### Release Scoping labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Priority labels ### Priority labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Severity labels ### Severity labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
@ -250,17 +122,14 @@ This [documentation](doc/development/contributing/issue_workflow.md) has been mo
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Label for community contributors ### Label for community contributors
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
## Implement design & UI elements ## Implement design & UI elements
This [documentation](doc/development/contributing/design.md) has been moved. This [documentation](doc/development/contributing/design.md) has been moved.
## Issue tracker ## Issue tracker
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
@ -269,7 +138,6 @@ This [documentation](doc/development/contributing/issue_workflow.md) has been mo
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Feature proposals ### Feature proposals
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
@ -278,32 +146,26 @@ This [documentation](doc/development/contributing/issue_workflow.md) has been mo
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Issue weight ### Issue weight
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Regression issues ### Regression issues
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Technical and UX debt ### Technical and UX debt
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Stewardship ### Stewardship
This [documentation](doc/development/contributing/issue_workflow.md) has been moved. This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
## Merge requests ## Merge requests
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved. This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
### Merge request guidelines ### Merge request guidelines
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved. This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
@ -313,12 +175,10 @@ This [documentation](doc/development/contributing/merge_request_workflow.md) has
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved. This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
## Definition of done ## Definition of done
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved. This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
## Style guides ## Style guides
This [documentation](doc/development/contributing/design.md) has been moved. This [documentation](doc/development/contributing/design.md) has been moved.

View file

@ -1,3 +1,4 @@
danger.import_plugin('danger/plugins/helper.rb')
danger.import_dangerfile(path: 'danger/metadata') danger.import_dangerfile(path: 'danger/metadata')
danger.import_dangerfile(path: 'danger/changes_size') danger.import_dangerfile(path: 'danger/changes_size')
danger.import_dangerfile(path: 'danger/changelog') danger.import_dangerfile(path: 'danger/changelog')

4
Dockerfile.assets Normal file
View file

@ -0,0 +1,4 @@
# Simple container to store assets for later use
FROM scratch
ADD public/assets /assets/
CMD /bin/true

View file

@ -1 +1 @@
0.125.1 0.129.0

View file

@ -1 +1 @@
1.1.1 1.3.1

View file

@ -1 +1 @@
8.3.3 8.4.1

View file

@ -1 +1 @@
7.0.1 7.1.3

28
Gemfile
View file

@ -79,13 +79,6 @@ gem 'gpgme'
gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap' gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap'
gem 'net-ldap' gem 'net-ldap'
# Git Wiki
# Only used to compute wiki page slugs
gem 'gitlab-gollum-lib', '~> 4.2', require: false
# Language detection
gem 'github-linguist', '~> 5.3.3', require: 'linguist'
# API # API
gem 'grape', '~> 1.1' gem 'grape', '~> 1.1'
gem 'grape-entity', '~> 0.7.1' gem 'grape-entity', '~> 0.7.1'
@ -146,6 +139,7 @@ gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.9' gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0' gem 'bootstrap_form', '~> 2.7.0'
gem 'nokogiri', '~> 1.8.2' gem 'nokogiri', '~> 1.8.2'
gem 'escape_utils', '~> 1.1'
# Calendar rendering # Calendar rendering
gem 'icalendar' gem 'icalendar'
@ -159,6 +153,11 @@ group :unicorn do
gem 'unicorn-worker-killer', '~> 0.4.4' gem 'unicorn-worker-killer', '~> 0.4.4'
end end
group :puma do
gem 'puma', '~> 3.12', require: false
gem 'puma_worker_killer', require: false
end
# State machine # State machine
gem 'state_machines-activerecord', '~> 0.5.1' gem 'state_machines-activerecord', '~> 0.5.1'
@ -212,7 +211,7 @@ gem 'hipchat', '~> 1.5.0'
gem 'jira-ruby', '~> 1.4' gem 'jira-ruby', '~> 1.4'
# Flowdock integration # Flowdock integration
gem 'gitlab-flowdock-git-hook', '~> 1.0.1' gem 'flowdock', '~> 0.7'
# Slack integration # Slack integration
gem 'slack-notifier', '~> 1.5.1' gem 'slack-notifier', '~> 1.5.1'
@ -245,9 +244,6 @@ gem 'rack-attack', '~> 4.4.1'
# Ace editor # Ace editor
gem 'ace-rails-ap', '~> 4.1.0' gem 'ace-rails-ap', '~> 4.1.0'
# Keyboard shortcuts
gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding # Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.5' gem 'charlock_holmes', '~> 0.7.5'
@ -420,11 +416,10 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly' gem 'gitaly-proto', '~> 0.123.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0' gem 'grpc', '~> 1.15.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed gem 'google-protobuf', '~> 3.6'
gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 1.0.0', require: false gem 'toml-rb', '~> 1.0.0', require: false
@ -436,6 +431,3 @@ gem 'flipper-active_support_cache_store', '~> 0.13.0'
# Structured logging # Structured logging
gem 'lograge', '~> 0.5' gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7' gem 'grape_logging', '~> 1.7'
# Asset synchronization
gem 'asset_sync', '~> 2.4'

View file

@ -58,11 +58,6 @@ GEM
asciidoctor (1.5.6.2) asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.8) asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5) asciidoctor (~> 1.5)
asset_sync (2.4.0)
activemodel (>= 4.1.0)
fog-core
mime-types (>= 2.99)
unf
ast (2.4.0) ast (2.4.0)
atomic (1.1.99) atomic (1.1.99)
attr_encrypted (3.1.0) attr_encrypted (3.1.0)
@ -274,32 +269,9 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.118.1) gitaly-proto (0.123.0)
google-protobuf (~> 3.1) grpc (~> 1.0)
grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.25.1)
github-markup (1.7.0) github-markup (1.7.0)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1)
sanitize (~> 4.6.4)
stringex (~> 2.6)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.4) gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0) gitlab-sidekiq-fetcher (0.3.0)
sidekiq (~> 5) sidekiq (~> 5)
@ -314,8 +286,6 @@ GEM
rubyntlm (~> 0.5) rubyntlm (~> 0.5)
globalid (0.4.1) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gon (6.2.0) gon (6.2.0)
actionpack (>= 3.0) actionpack (>= 3.0)
multi_json multi_json
@ -327,16 +297,15 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1) google-protobuf (3.6.1)
googleapis-common-protos-types (1.0.1) googleapis-common-protos-types (1.0.2)
google-protobuf (~> 3.0) google-protobuf (~> 3.0)
googleauth (0.6.2) googleauth (0.6.6)
faraday (~> 0.12) faraday (~> 0.12)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
logging (~> 2.0)
memoist (~> 0.12) memoist (~> 0.12)
multi_json (~> 1.11) multi_json (~> 1.11)
os (~> 0.9) os (>= 0.9, < 2.0)
signet (~> 0.7) signet (~> 0.7)
gpgme (2.0.13) gpgme (2.0.13)
mini_portile2 (~> 2.1) mini_portile2 (~> 2.1)
@ -360,10 +329,9 @@ GEM
railties railties
sprockets-rails sprockets-rails
graphql (1.8.1) graphql (1.8.1)
grpc (1.11.0) grpc (1.15.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
haml (5.0.4) haml (5.0.4)
temple (>= 0.8.0) temple (>= 0.8.0)
tilt tilt
@ -465,11 +433,7 @@ GEM
xml-simple xml-simple
licensee (8.9.2) licensee (8.9.2)
rugged (~> 0.24) rugged (~> 0.24)
little-plugger (1.1.4)
locale (2.1.2) locale (2.1.2)
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
lograge (0.10.0) lograge (0.10.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
@ -493,7 +457,6 @@ GEM
mini_mime (1.0.1) mini_mime (1.0.1)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.7.0) minitest (5.7.0)
mousetrap-rails (1.4.6)
msgpack (1.2.4) msgpack (1.2.4)
multi_json (1.13.1) multi_json (1.13.1)
multi_xml (0.6.0) multi_xml (0.6.0)
@ -575,9 +538,9 @@ GEM
org-ruby (0.9.12) org-ruby (0.9.12)
rubypants (~> 0.2) rubypants (~> 0.2)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (0.9.6) os (1.0.0)
parallel (1.12.1) parallel (1.12.1)
parser (2.5.1.0) parser (2.5.3.0)
ast (~> 2.4.0) ast (~> 2.4.0)
parslet (1.8.2) parslet (1.8.2)
peek (1.0.1) peek (1.0.1)
@ -605,7 +568,6 @@ GEM
pg (0.18.4) pg (0.18.4)
po_to_json (1.0.1) po_to_json (1.0.1)
json (>= 1.6.0) json (>= 1.6.0)
posix-spawn (0.3.13)
powerpack (0.1.1) powerpack (0.1.1)
premailer (1.10.4) premailer (1.10.4)
addressable addressable
@ -629,6 +591,10 @@ GEM
pry-rails (0.3.6) pry-rails (0.3.6)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (3.0.3) public_suffix (3.0.3)
puma (3.12.0)
puma_worker_killer (0.1.0)
get_process_mem (~> 0.2)
puma (>= 2.7, < 4)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.10) rack (1.6.10)
rack-accept (0.4.5) rack-accept (0.4.5)
@ -797,7 +763,7 @@ GEM
rubyzip (1.2.2) rubyzip (1.2.2)
rufus-scheduler (3.4.0) rufus-scheduler (3.4.0)
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.4) rugged (0.27.5)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.6) sanitize (4.6.6)
crass (~> 1.0.2) crass (~> 1.0.2)
@ -843,7 +809,7 @@ GEM
sidekiq-cron (0.6.0) sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0) rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1) sidekiq (>= 4.2.1)
signet (0.8.1) signet (0.11.0)
addressable (~> 2.3) addressable (~> 2.3)
faraday (~> 0.9) faraday (~> 0.9)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
@ -876,7 +842,6 @@ GEM
state_machines-activerecord (0.5.1) state_machines-activerecord (0.5.1)
activerecord (>= 4.1, < 6.0) activerecord (>= 4.1, < 6.0)
state_machines-activemodel (>= 0.5.0) state_machines-activemodel (>= 0.5.0)
stringex (2.8.4)
sys-filesystem (1.1.6) sys-filesystem (1.1.6)
ffi ffi
sysexits (1.2.0) sysexits (1.2.0)
@ -968,7 +933,6 @@ DEPENDENCIES
asana (~> 0.6.0) asana (~> 0.6.0)
asciidoctor (~> 1.5.6) asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8) asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0) attr_encrypted (~> 3.1.0)
awesome_print awesome_print
babosa (~> 1.0.2) babosa (~> 1.0.2)
@ -1006,6 +970,7 @@ DEPENDENCIES
ed25519 (~> 1.2) ed25519 (~> 1.2)
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 2.2.0) email_spec (~> 2.2.0)
escape_utils (~> 1.1)
factory_bot_rails (~> 4.8.2) factory_bot_rails (~> 4.8.2)
faraday (~> 0.12) faraday (~> 0.12)
fast_blank fast_blank
@ -1013,6 +978,7 @@ DEPENDENCIES
flipper (~> 0.13.0) flipper (~> 0.13.0)
flipper-active_record (~> 0.13.0) flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0) flipper-active_support_cache_store (~> 0.13.0)
flowdock (~> 0.7)
fog-aliyun (~> 0.2.0) fog-aliyun (~> 0.2.0)
fog-aws (~> 2.0.1) fog-aws (~> 2.0.1)
fog-core (~> 1.44) fog-core (~> 1.44)
@ -1027,18 +993,15 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.118.1) gitaly-proto (~> 0.123.0)
github-linguist (~> 5.3.3)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-markup (~> 1.6.4) gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4) gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2) gon (~> 6.2)
google-api-client (~> 0.23) google-api-client (~> 0.23)
google-protobuf (= 3.5.1) google-protobuf (~> 3.6)
gpgme gpgme
grape (~> 1.1) grape (~> 1.1)
grape-entity (~> 0.7.1) grape-entity (~> 0.7.1)
@ -1046,7 +1009,7 @@ DEPENDENCIES
grape_logging (~> 1.7) grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10) graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0) graphql (~> 1.8.0)
grpc (~> 1.11.0) grpc (~> 1.15.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.8.8) hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5) hangouts-chat (~> 0.0.5)
@ -1075,7 +1038,6 @@ DEPENDENCIES
method_source (~> 0.8) method_source (~> 0.8)
mini_magick mini_magick
minitest (~> 5.7.0) minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10) mysql2 (~> 0.4.10)
net-ldap net-ldap
net-ssh (~> 5.0) net-ssh (~> 5.0)
@ -1109,6 +1071,8 @@ DEPENDENCIES
prometheus-client-mmap (~> 0.9.4) prometheus-client-mmap (~> 0.9.4)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
puma (~> 3.12)
puma_worker_killer
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0) rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
@ -1187,4 +1151,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.16.4 1.17.1

View file

@ -61,11 +61,6 @@ GEM
asciidoctor (1.5.6.2) asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.8) asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5) asciidoctor (~> 1.5)
asset_sync (2.4.0)
activemodel (>= 4.1.0)
fog-core
mime-types (>= 2.99)
unf
ast (2.4.0) ast (2.4.0)
atomic (1.1.99) atomic (1.1.99)
attr_encrypted (3.1.0) attr_encrypted (3.1.0)
@ -277,32 +272,9 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.118.1) gitaly-proto (0.123.0)
google-protobuf (~> 3.1) grpc (~> 1.0)
grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.25.1)
github-markup (1.7.0) github-markup (1.7.0)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1)
sanitize (~> 4.6.4)
stringex (~> 2.6)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.4) gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0) gitlab-sidekiq-fetcher (0.3.0)
sidekiq (~> 5) sidekiq (~> 5)
@ -317,8 +289,6 @@ GEM
rubyntlm (~> 0.5) rubyntlm (~> 0.5)
globalid (0.4.1) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gon (6.2.0) gon (6.2.0)
actionpack (>= 3.0) actionpack (>= 3.0)
multi_json multi_json
@ -330,16 +300,15 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1) google-protobuf (3.6.1)
googleapis-common-protos-types (1.0.1) googleapis-common-protos-types (1.0.2)
google-protobuf (~> 3.0) google-protobuf (~> 3.0)
googleauth (0.6.2) googleauth (0.6.6)
faraday (~> 0.12) faraday (~> 0.12)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
logging (~> 2.0)
memoist (~> 0.12) memoist (~> 0.12)
multi_json (~> 1.11) multi_json (~> 1.11)
os (~> 0.9) os (>= 0.9, < 2.0)
signet (~> 0.7) signet (~> 0.7)
gpgme (2.0.13) gpgme (2.0.13)
mini_portile2 (~> 2.1) mini_portile2 (~> 2.1)
@ -363,10 +332,9 @@ GEM
railties railties
sprockets-rails sprockets-rails
graphql (1.8.1) graphql (1.8.1)
grpc (1.11.0) grpc (1.15.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
haml (5.0.4) haml (5.0.4)
temple (>= 0.8.0) temple (>= 0.8.0)
tilt tilt
@ -468,11 +436,7 @@ GEM
xml-simple xml-simple
licensee (8.9.2) licensee (8.9.2)
rugged (~> 0.24) rugged (~> 0.24)
little-plugger (1.1.4)
locale (2.1.2) locale (2.1.2)
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
lograge (0.10.0) lograge (0.10.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
@ -496,7 +460,6 @@ GEM
mini_mime (1.0.1) mini_mime (1.0.1)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.7.0) minitest (5.7.0)
mousetrap-rails (1.4.6)
msgpack (1.2.4) msgpack (1.2.4)
multi_json (1.13.1) multi_json (1.13.1)
multi_xml (0.6.0) multi_xml (0.6.0)
@ -579,9 +542,9 @@ GEM
org-ruby (0.9.12) org-ruby (0.9.12)
rubypants (~> 0.2) rubypants (~> 0.2)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (0.9.6) os (1.0.0)
parallel (1.12.1) parallel (1.12.1)
parser (2.5.1.0) parser (2.5.1.2)
ast (~> 2.4.0) ast (~> 2.4.0)
parslet (1.8.2) parslet (1.8.2)
peek (1.0.1) peek (1.0.1)
@ -609,7 +572,6 @@ GEM
pg (0.18.4) pg (0.18.4)
po_to_json (1.0.1) po_to_json (1.0.1)
json (>= 1.6.0) json (>= 1.6.0)
posix-spawn (0.3.13)
powerpack (0.1.1) powerpack (0.1.1)
premailer (1.10.4) premailer (1.10.4)
addressable addressable
@ -633,6 +595,10 @@ GEM
pry-rails (0.3.6) pry-rails (0.3.6)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (3.0.3) public_suffix (3.0.3)
puma (3.12.0)
puma_worker_killer (0.1.0)
get_process_mem (~> 0.2)
puma (>= 2.7, < 4)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (2.0.5) rack (2.0.5)
rack-accept (0.4.5) rack-accept (0.4.5)
@ -805,7 +771,7 @@ GEM
rubyzip (1.2.2) rubyzip (1.2.2)
rufus-scheduler (3.4.0) rufus-scheduler (3.4.0)
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.4) rugged (0.27.5)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.6) sanitize (4.6.6)
crass (~> 1.0.2) crass (~> 1.0.2)
@ -851,7 +817,7 @@ GEM
sidekiq-cron (0.6.0) sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0) rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1) sidekiq (>= 4.2.1)
signet (0.8.1) signet (0.11.0)
addressable (~> 2.3) addressable (~> 2.3)
faraday (~> 0.9) faraday (~> 0.9)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
@ -884,7 +850,6 @@ GEM
state_machines-activerecord (0.5.1) state_machines-activerecord (0.5.1)
activerecord (>= 4.1, < 6.0) activerecord (>= 4.1, < 6.0)
state_machines-activemodel (>= 0.5.0) state_machines-activemodel (>= 0.5.0)
stringex (2.8.4)
sys-filesystem (1.1.6) sys-filesystem (1.1.6)
ffi ffi
sysexits (1.2.0) sysexits (1.2.0)
@ -977,7 +942,6 @@ DEPENDENCIES
asana (~> 0.6.0) asana (~> 0.6.0)
asciidoctor (~> 1.5.6) asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8) asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0) attr_encrypted (~> 3.1.0)
awesome_print awesome_print
babosa (~> 1.0.2) babosa (~> 1.0.2)
@ -1015,6 +979,7 @@ DEPENDENCIES
ed25519 (~> 1.2) ed25519 (~> 1.2)
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 2.2.0) email_spec (~> 2.2.0)
escape_utils (~> 1.1)
factory_bot_rails (~> 4.8.2) factory_bot_rails (~> 4.8.2)
faraday (~> 0.12) faraday (~> 0.12)
fast_blank fast_blank
@ -1022,6 +987,7 @@ DEPENDENCIES
flipper (~> 0.13.0) flipper (~> 0.13.0)
flipper-active_record (~> 0.13.0) flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0) flipper-active_support_cache_store (~> 0.13.0)
flowdock (~> 0.7)
fog-aliyun (~> 0.2.0) fog-aliyun (~> 0.2.0)
fog-aws (~> 2.0.1) fog-aws (~> 2.0.1)
fog-core (~> 1.44) fog-core (~> 1.44)
@ -1036,18 +1002,15 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.118.1) gitaly-proto (~> 0.123.0)
github-linguist (~> 5.3.3)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-markup (~> 1.6.4) gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher gitlab-sidekiq-fetcher
gitlab-styles (~> 2.4) gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2) gon (~> 6.2)
google-api-client (~> 0.23) google-api-client (~> 0.23)
google-protobuf (= 3.5.1) google-protobuf (~> 3.6)
gpgme gpgme
grape (~> 1.1) grape (~> 1.1)
grape-entity (~> 0.7.1) grape-entity (~> 0.7.1)
@ -1055,7 +1018,7 @@ DEPENDENCIES
grape_logging (~> 1.7) grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10) graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0) graphql (~> 1.8.0)
grpc (~> 1.11.0) grpc (~> 1.15.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.8.8) hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5) hangouts-chat (~> 0.0.5)
@ -1084,7 +1047,6 @@ DEPENDENCIES
method_source (~> 0.8) method_source (~> 0.8)
mini_magick mini_magick
minitest (~> 5.7.0) minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10) mysql2 (~> 0.4.10)
net-ldap net-ldap
net-ssh (~> 5.0) net-ssh (~> 5.0)
@ -1118,6 +1080,8 @@ DEPENDENCIES
prometheus-client-mmap (~> 0.9.4) prometheus-client-mmap (~> 0.9.4)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
puma (~> 3.12)
puma_worker_killer
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0) rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
@ -1196,4 +1160,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.16.4 1.17.1

View file

@ -208,6 +208,7 @@ the stable branch are:
* Fixes or improvements to automated QA scenarios * Fixes or improvements to automated QA scenarios
* [Documentation updates](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late) for changes in the same release * [Documentation updates](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late) for changes in the same release
* New or updated translations (as long as they do not touch application code) * New or updated translations (as long as they do not touch application code)
* Changes that are behind a feature flag and have the ~"feature flag" label
During the feature freeze all merge requests that are meant to go into the During the feature freeze all merge requests that are meant to go into the
upcoming release should have the correct milestone assigned _and_ the upcoming release should have the correct milestone assigned _and_ the

View file

@ -83,7 +83,7 @@ Instructions on how to start GitLab and how to run the tests can be found in the
GitLab is a Ruby on Rails application that runs on the following software: GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL/OpenSUSE - Ubuntu/Debian/CentOS/RHEL/OpenSUSE
- Ruby (MRI) 2.3 - Ruby (MRI) 2.4
- Git 2.8.4+ - Git 2.8.4+
- Redis 2.8+ - Redis 2.8+
- PostgreSQL (preferred) or MySQL - PostgreSQL (preferred) or MySQL

View file

@ -1 +1 @@
11.4.9 11.5.3

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14">
<g fill="#d6d7d9">
<path d="M8.7 0L5.3.3l3.2 6.8-3.2 6.6 3.5.3L12 6.9z"/>
<ellipse cx="1.7" cy="11.1" rx="1.7" ry="1.7"/>
<ellipse cx="1.7" cy="5.6" rx="1.7" ry="1.7"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 312 B

View file

@ -6,10 +6,12 @@ import Pager from './pager';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
export default class Activities { export default class Activities {
constructor() { constructor(container = '') {
Pager.init(20, true, false, data => data, this.updateTooltips); this.container = container;
$('.event-filter-link').on('click', (e) => { Pager.init(20, true, false, data => data, this.updateTooltips, this.container);
$('.event-filter-link').on('click', e => {
e.preventDefault(); e.preventDefault();
this.toggleFilter(e.currentTarget); this.toggleFilter(e.currentTarget);
this.reloadActivities(); this.reloadActivities();
@ -22,7 +24,7 @@ export default class Activities {
reloadActivities() { reloadActivities() {
$('.content_list').html(''); $('.content_list').html('');
Pager.init(20, true, false, data => data, this.updateTooltips); Pager.init(20, true, false, data => data, this.updateTooltips, this.container);
} }
toggleFilter(sender) { toggleFilter(sender) {

View file

@ -1,12 +1,14 @@
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip'; import Tooltip from '~/vue_shared/directives/tooltip';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
name: 'Badge', name: 'Badge',
components: { components: {
Icon, Icon,
Tooltip, Tooltip,
GlLoadingIcon,
}, },
directives: { directives: {
Tooltip, Tooltip,

View file

@ -4,6 +4,7 @@ import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import createEmptyBadge from '../empty_badge'; import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue'; import Badge from './badge.vue';
@ -14,6 +15,7 @@ export default {
components: { components: {
Badge, Badge,
LoadingButton, LoadingButton,
GlLoadingIcon,
}, },
props: { props: {
isEditing: { isEditing: {

View file

@ -1,5 +1,6 @@
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import BadgeListRow from './badge_list_row.vue'; import BadgeListRow from './badge_list_row.vue';
import { GROUP_BADGE } from '../constants'; import { GROUP_BADGE } from '../constants';
@ -7,6 +8,7 @@ export default {
name: 'BadgeList', name: 'BadgeList',
components: { components: {
BadgeListRow, BadgeListRow,
GlLoadingIcon,
}, },
computed: { computed: {
...mapState(['badges', 'isLoading', 'kind']), ...mapState(['badges', 'isLoading', 'kind']),

View file

@ -2,6 +2,7 @@
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { PROJECT_BADGE } from '../constants'; import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue'; import Badge from './badge.vue';
@ -10,6 +11,7 @@ export default {
components: { components: {
Badge, Badge,
Icon, Icon,
GlLoadingIcon,
}, },
props: { props: {
badge: { badge: {

View file

@ -51,7 +51,7 @@ export default function initCopyToClipboard() {
* the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy * the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy
* data types to the intended values. * data types to the intended values.
*/ */
$(document).on('copy', 'body > textarea[readonly]', (e) => { $(document).on('copy', 'body > textarea[readonly]', e => {
const { clipboardData } = e.originalEvent; const { clipboardData } = e.originalEvent;
if (!clipboardData) return; if (!clipboardData) return;

View file

@ -2,7 +2,9 @@ import $ from 'jquery';
$(() => { $(() => {
$('body').on('click', '.js-details-target', function target() { $('body').on('click', '.js-details-target', function target() {
$(this).closest('.js-details-container').toggleClass('open'); $(this)
.closest('.js-details-container')
.toggleClass('open');
}); });
// Show details content. Hides link after click. // Show details content. Hides link after click.
@ -13,7 +15,9 @@ $(() => {
// //
$('body').on('click', '.js-details-expand', function expand(e) { $('body').on('click', '.js-details-expand', function expand(e) {
e.preventDefault(); e.preventDefault();
$(this).next('.js-details-content').removeClass('hide'); $(this)
.next('.js-details-content')
.removeClass('hide');
$(this).hide(); $(this).hide();
const truncatedItem = $(this).siblings('.js-details-short'); const truncatedItem = $(this).siblings('.js-details-short');

View file

@ -1,4 +1,4 @@
/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, max-len, no-restricted-syntax, guard-for-in, no-continue */ /* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, no-restricted-syntax, guard-for-in, no-continue */
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
@ -34,7 +34,7 @@ const gfmRules = {
}, },
}, },
AutolinkFilter: { AutolinkFilter: {
'a'(el, text) { a(el, text) {
// Fallback on the regular MarkdownFilter's `a` handler. // Fallback on the regular MarkdownFilter's `a` handler.
if (text !== el.getAttribute('href')) return false; if (text !== el.getAttribute('href')) return false;
@ -60,7 +60,7 @@ const gfmRules = {
}, },
}, },
ImageLazyLoadFilter: { ImageLazyLoadFilter: {
'img'(el, text) { img(el, text) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
}, },
}, },
@ -71,7 +71,7 @@ const gfmRules = {
return CopyAsGFM.nodeToGFM(videoEl); return CopyAsGFM.nodeToGFM(videoEl);
}, },
'video'(el) { video(el) {
return `![${el.dataset.title}](${el.getAttribute('src')})`; return `![${el.dataset.title}](${el.getAttribute('src')})`;
}, },
}, },
@ -118,11 +118,14 @@ const gfmRules = {
'a[name]:not([href]):empty'(el) { 'a[name]:not([href]):empty'(el) {
return el.outerHTML; return el.outerHTML;
}, },
'dl'(el, text) { dl(el, text) {
let lines = text.replace(/\n\n/g, '\n').trim().split('\n'); let lines = text
.replace(/\n\n/g, '\n')
.trim()
.split('\n');
// Add two spaces to the front of subsequent list items lines, // Add two spaces to the front of subsequent list items lines,
// or leave the line entirely blank. // or leave the line entirely blank.
lines = lines.map((l) => { lines = lines.map(l => {
const line = l.trim(); const line = l.trim();
if (line.length === 0) return ''; if (line.length === 0) return '';
@ -151,27 +154,30 @@ const gfmRules = {
// Prefixes lines with 4 spaces if the code contains triple backticks // Prefixes lines with 4 spaces if the code contains triple backticks
if (lang === '' && text.match(/^```/gm)) { if (lang === '' && text.match(/^```/gm)) {
return text.split('\n').map((l) => { return text
const line = l.trim(); .split('\n')
if (line.length === 0) return ''; .map(l => {
const line = l.trim();
if (line.length === 0) return '';
return ` ${line}`; return ` ${line}`;
}).join('\n'); })
.join('\n');
} }
return `\`\`\`${lang}\n${text}\n\`\`\``; return `\`\`\`${lang}\n${text}\n\`\`\``;
}, },
'pre > code'(el, text) { 'pre > code'(el, text) {
// Don't wrap code blocks in `` // Don't wrap code blocks in ``
return text; return text;
}, },
}, },
MarkdownFilter: { MarkdownFilter: {
'br'(el) { br(el) {
// Two spaces at the end of a line are turned into a BR // Two spaces at the end of a line are turned into a BR
return ' '; return ' ';
}, },
'code'(el, text) { code(el, text) {
let backtickCount = 1; let backtickCount = 1;
const backtickMatch = text.match(/`+/); const backtickMatch = text.match(/`+/);
if (backtickMatch) { if (backtickMatch) {
@ -183,27 +189,31 @@ const gfmRules = {
return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks; return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
}, },
'blockquote'(el, text) { blockquote(el, text) {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); return text
.trim()
.split('\n')
.map(s => `> ${s}`.trim())
.join('\n');
}, },
'img'(el) { img(el) {
const imageSrc = el.src; const imageSrc = el.src;
const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || ''); const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
return `![${el.getAttribute('alt')}](${imageUrl})`; return `![${el.getAttribute('alt')}](${imageUrl})`;
}, },
'a.anchor'(el, text) { 'a.anchor'(el, text) {
// Don't render a Markdown link for the anchor link inside a heading // Don't render a Markdown link for the anchor link inside a heading
return text; return text;
}, },
'a'(el, text) { a(el, text) {
return `[${text}](${el.getAttribute('href')})`; return `[${text}](${el.getAttribute('href')})`;
}, },
'li'(el, text) { li(el, text) {
const lines = text.trim().split('\n'); const lines = text.trim().split('\n');
const firstLine = `- ${lines.shift()}`; const firstLine = `- ${lines.shift()}`;
// Add four spaces to the front of subsequent list items lines, // Add four spaces to the front of subsequent list items lines,
// or leave the line entirely blank. // or leave the line entirely blank.
const nextLines = lines.map((s) => { const nextLines = lines.map(s => {
if (s.trim().length === 0) return ''; if (s.trim().length === 0) return '';
return ` ${s}`; return ` ${s}`;
@ -211,49 +221,49 @@ const gfmRules = {
return `${firstLine}\n${nextLines.join('\n')}`; return `${firstLine}\n${nextLines.join('\n')}`;
}, },
'ul'(el, text) { ul(el, text) {
return text; return text;
}, },
'ol'(el, text) { ol(el, text) {
// LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists. // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
return text.replace(/^- /mg, '1. '); return text.replace(/^- /gm, '1. ');
}, },
'h1'(el, text) { h1(el, text) {
return `# ${text.trim()}\n`; return `# ${text.trim()}\n`;
}, },
'h2'(el, text) { h2(el, text) {
return `## ${text.trim()}\n`; return `## ${text.trim()}\n`;
}, },
'h3'(el, text) { h3(el, text) {
return `### ${text.trim()}\n`; return `### ${text.trim()}\n`;
}, },
'h4'(el, text) { h4(el, text) {
return `#### ${text.trim()}\n`; return `#### ${text.trim()}\n`;
}, },
'h5'(el, text) { h5(el, text) {
return `##### ${text.trim()}\n`; return `##### ${text.trim()}\n`;
}, },
'h6'(el, text) { h6(el, text) {
return `###### ${text.trim()}\n`; return `###### ${text.trim()}\n`;
}, },
'strong'(el, text) { strong(el, text) {
return `**${text}**`; return `**${text}**`;
}, },
'em'(el, text) { em(el, text) {
return `_${text}_`; return `_${text}_`;
}, },
'del'(el, text) { del(el, text) {
return `~~${text}~~`; return `~~${text}~~`;
}, },
'hr'(el) { hr(el) {
// extra leading \n is to ensure that there is a blank line between // extra leading \n is to ensure that there is a blank line between
// a list followed by an hr, otherwise this breaks old redcarpet rendering // a list followed by an hr, otherwise this breaks old redcarpet rendering
return '\n-----\n'; return '\n-----\n';
}, },
'p'(el, text) { p(el, text) {
return `${text.trim()}\n`; return `${text.trim()}\n`;
}, },
'table'(el) { table(el) {
const theadEl = el.querySelector('thead'); const theadEl = el.querySelector('thead');
const tbodyEl = el.querySelector('tbody'); const tbodyEl = el.querySelector('tbody');
if (!theadEl || !tbodyEl) return false; if (!theadEl || !tbodyEl) return false;
@ -263,8 +273,8 @@ const gfmRules = {
return [theadText, tbodyText].join('\n'); return [theadText, tbodyText].join('\n');
}, },
'thead'(el, text) { thead(el, text) {
const cells = _.map(el.querySelectorAll('th'), (cell) => { const cells = _.map(el.querySelectorAll('th'), cell => {
let chars = CopyAsGFM.nodeToGFM(cell).length + 2; let chars = CopyAsGFM.nodeToGFM(cell).length + 2;
let before = ''; let before = '';
@ -296,7 +306,7 @@ const gfmRules = {
return [text, separatorRow].join('\n'); return [text, separatorRow].join('\n');
}, },
'tr'(el) { tr(el) {
const cellEls = el.querySelectorAll('td, th'); const cellEls = el.querySelectorAll('td, th');
if (cellEls.length === 0) return false; if (cellEls.length === 0) return false;
@ -315,8 +325,12 @@ export class CopyAsGFM {
const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent); const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent);
if (isIOS) return; if (isIOS) return;
$(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); $(document).on('copy', '.md, .wiki', e => {
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection);
});
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', e => {
CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection);
});
$(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM); $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM);
} }
@ -356,7 +370,7 @@ export class CopyAsGFM {
// This will break down when the actual code block contains an uneven // This will break down when the actual code block contains an uneven
// number of backticks, but this is a rare edge case. // number of backticks, but this is a rare edge case.
const backtickMatch = textBefore.match(/`/g); const backtickMatch = textBefore.match(/`/g);
const insideCodeBlock = backtickMatch && (backtickMatch.length % 2) === 1; const insideCodeBlock = backtickMatch && backtickMatch.length % 2 === 1;
if (insideCodeBlock) { if (insideCodeBlock) {
return text; return text;
@ -393,7 +407,9 @@ export class CopyAsGFM {
let lineSelector = '.line'; let lineSelector = '.line';
if (target) { if (target) {
const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0]; const lineClass = ['left-side', 'right-side'].filter(name =>
target.classList.contains(name),
)[0];
if (lineClass) { if (lineClass) {
lineSelector = `.line_content.${lineClass} ${lineSelector}`; lineSelector = `.line_content.${lineClass} ${lineSelector}`;
} }
@ -436,7 +452,8 @@ export class CopyAsGFM {
return node.textContent; return node.textContent;
} }
const respectWhitespace = respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE'); const respectWhitespace =
respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE');
const text = this.innerGFM(node, respectWhitespace); const text = this.innerGFM(node, respectWhitespace);

View file

@ -32,7 +32,9 @@ export default function renderMath($els) {
Promise.all([ Promise.all([
import(/* webpackChunkName: 'katex' */ 'katex'), import(/* webpackChunkName: 'katex' */ 'katex'),
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'), import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
]).then(([katex]) => { ])
renderWithKaTeX($els, katex); .then(([katex]) => {
}).catch(() => flash(__('An error occurred while rendering KaTeX'))); renderWithKaTeX($els, katex);
})
.catch(() => flash(__('An error occurred while rendering KaTeX')));
} }

View file

@ -17,44 +17,46 @@ import flash from '~/flash';
export default function renderMermaid($els) { export default function renderMermaid($els) {
if (!$els.length) return; if (!$els.length) return;
import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => { import(/* webpackChunkName: 'mermaid' */ 'mermaid')
mermaid.initialize({ .then(mermaid => {
// mermaid core options mermaid.initialize({
mermaid: { // mermaid core options
startOnLoad: false, mermaid: {
}, startOnLoad: false,
// mermaidAPI options },
theme: 'neutral', // mermaidAPI options
flowchart: { theme: 'neutral',
htmlLabels: false, flowchart: {
}, htmlLabels: false,
}); },
$els.each((i, el) => {
const source = el.textContent;
// Remove any extra spans added by the backend syntax highlighting.
Object.assign(el, { textContent: source });
mermaid.init(undefined, el, (id) => {
const svg = document.getElementById(id);
svg.classList.add('mermaid');
// pre > code > svg
svg.closest('pre').replaceWith(svg);
// We need to add the original source into the DOM to allow Copy-as-GFM
// to access it.
const sourceEl = document.createElement('text');
sourceEl.classList.add('source');
sourceEl.setAttribute('display', 'none');
sourceEl.textContent = source;
svg.appendChild(sourceEl);
}); });
$els.each((i, el) => {
const source = el.textContent;
// Remove any extra spans added by the backend syntax highlighting.
Object.assign(el, { textContent: source });
mermaid.init(undefined, el, id => {
const svg = document.getElementById(id);
svg.classList.add('mermaid');
// pre > code > svg
svg.closest('pre').replaceWith(svg);
// We need to add the original source into the DOM to allow Copy-as-GFM
// to access it.
const sourceEl = document.createElement('text');
sourceEl.classList.add('source');
sourceEl.setAttribute('display', 'none');
sourceEl.textContent = source;
svg.appendChild(sourceEl);
});
});
})
.catch(err => {
flash(`Can't load mermaid module: ${err}`);
}); });
}).catch((err) => {
flash(`Can't load mermaid module: ${err}`);
});
} }

View file

@ -26,7 +26,7 @@ MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.';
MarkdownPreview.prototype.ajaxCache = {}; MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function ($form) { MarkdownPreview.prototype.showPreview = function($form) {
var mdText; var mdText;
var markdownVersion; var markdownVersion;
var url; var url;
@ -44,34 +44,40 @@ MarkdownPreview.prototype.showPreview = function ($form) {
this.hideReferencedUsers($form); this.hideReferencedUsers($form);
} else { } else {
preview.addClass('md-preview-loading').text('Loading...'); preview.addClass('md-preview-loading').text('Loading...');
this.fetchMarkdownPreview(mdText, url, (function (response) { this.fetchMarkdownPreview(
var body; mdText,
if (response.body.length > 0) { url,
({ body } = response); function(response) {
} else { var body;
body = this.emptyMessage; if (response.body.length > 0) {
} ({ body } = response);
} else {
body = this.emptyMessage;
}
preview.removeClass('md-preview-loading').html(body); preview.removeClass('md-preview-loading').html(body);
preview.renderGFM(); preview.renderGFM();
this.renderReferencedUsers(response.references.users, $form); this.renderReferencedUsers(response.references.users, $form);
if (response.references.commands) { if (response.references.commands) {
this.renderReferencedCommands(response.references.commands, $form); this.renderReferencedCommands(response.references.commands, $form);
} }
}).bind(this)); }.bind(this),
);
} }
}; };
MarkdownPreview.prototype.versionedPreviewPath = function (markdownPreviewPath, markdownVersion) { MarkdownPreview.prototype.versionedPreviewPath = function(markdownPreviewPath, markdownVersion) {
if (typeof markdownVersion === 'undefined') { if (typeof markdownVersion === 'undefined') {
return markdownPreviewPath; return markdownPreviewPath;
} }
return `${markdownPreviewPath}${markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'}markdown_version=${markdownVersion}`; return `${markdownPreviewPath}${
markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
}markdown_version=${markdownVersion}`;
}; };
MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { MarkdownPreview.prototype.fetchMarkdownPreview = function(text, url, success) {
if (!url) { if (!url) {
return; return;
} }
@ -79,24 +85,25 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
success(this.ajaxCache.response); success(this.ajaxCache.response);
return; return;
} }
axios.post(url, { axios
text, .post(url, {
}) text,
.then(({ data }) => { })
this.ajaxCache = { .then(({ data }) => {
text: text, this.ajaxCache = {
response: data, text: text,
}; response: data,
success(data); };
}) success(data);
.catch(() => flash(__('An error occurred while fetching markdown preview'))); })
.catch(() => flash(__('An error occurred while fetching markdown preview')));
}; };
MarkdownPreview.prototype.hideReferencedUsers = function ($form) { MarkdownPreview.prototype.hideReferencedUsers = function($form) {
$form.find('.referenced-users').hide(); $form.find('.referenced-users').hide();
}; };
MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { MarkdownPreview.prototype.renderReferencedUsers = function(users, $form) {
var referencedUsers; var referencedUsers;
referencedUsers = $form.find('.referenced-users'); referencedUsers = $form.find('.referenced-users');
if (referencedUsers.length) { if (referencedUsers.length) {
@ -109,11 +116,11 @@ MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) {
} }
}; };
MarkdownPreview.prototype.hideReferencedCommands = function ($form) { MarkdownPreview.prototype.hideReferencedCommands = function($form) {
$form.find('.referenced-commands').hide(); $form.find('.referenced-commands').hide();
}; };
MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { MarkdownPreview.prototype.renderReferencedCommands = function(commands, $form) {
var referencedCommands; var referencedCommands;
referencedCommands = $form.find('.referenced-commands'); referencedCommands = $form.find('.referenced-commands');
if (commands.length > 0) { if (commands.length > 0) {
@ -132,14 +139,14 @@ writeButtonSelector = '.js-md-write-button';
lastTextareaPreviewed = null; lastTextareaPreviewed = null;
const markdownToolbar = $('.md-header-toolbar'); const markdownToolbar = $('.md-header-toolbar');
$.fn.setupMarkdownPreview = function () { $.fn.setupMarkdownPreview = function() {
var $form = $(this); var $form = $(this);
$form.find('textarea.markdown-area').on('input', function () { $form.find('textarea.markdown-area').on('input', function() {
markdownPreview.hideReferencedUsers($form); markdownPreview.hideReferencedUsers($form);
}); });
}; };
$(document).on('markdown-preview:show', function (e, $form) { $(document).on('markdown-preview:show', function(e, $form) {
if (!$form) { if (!$form) {
return; return;
} }
@ -148,8 +155,14 @@ $(document).on('markdown-preview:show', function (e, $form) {
lastTextareaHeight = lastTextareaPreviewed.height(); lastTextareaHeight = lastTextareaPreviewed.height();
// toggle tabs // toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active'); $form
$form.find(previewButtonSelector).parent().addClass('active'); .find(writeButtonSelector)
.parent()
.removeClass('active');
$form
.find(previewButtonSelector)
.parent()
.addClass('active');
// toggle content // toggle content
$form.find('.md-write-holder').hide(); $form.find('.md-write-holder').hide();
@ -158,7 +171,7 @@ $(document).on('markdown-preview:show', function (e, $form) {
markdownPreview.showPreview($form); markdownPreview.showPreview($form);
}); });
$(document).on('markdown-preview:hide', function (e, $form) { $(document).on('markdown-preview:hide', function(e, $form) {
if (!$form) { if (!$form) {
return; return;
} }
@ -169,8 +182,14 @@ $(document).on('markdown-preview:hide', function (e, $form) {
} }
// toggle tabs // toggle tabs
$form.find(writeButtonSelector).parent().addClass('active'); $form
$form.find(previewButtonSelector).parent().removeClass('active'); .find(writeButtonSelector)
.parent()
.addClass('active');
$form
.find(previewButtonSelector)
.parent()
.removeClass('active');
// toggle content // toggle content
$form.find('.md-write-holder').show(); $form.find('.md-write-holder').show();
@ -181,7 +200,7 @@ $(document).on('markdown-preview:hide', function (e, $form) {
markdownPreview.hideReferencedCommands($form); markdownPreview.hideReferencedCommands($form);
}); });
$(document).on('markdown-preview:toggle', function (e, keyboardEvent) { $(document).on('markdown-preview:toggle', function(e, keyboardEvent) {
var $target; var $target;
$target = $(keyboardEvent.target); $target = $(keyboardEvent.target);
if ($target.is('textarea.markdown-area')) { if ($target.is('textarea.markdown-area')) {
@ -194,14 +213,14 @@ $(document).on('markdown-preview:toggle', function (e, keyboardEvent) {
} }
}); });
$(document).on('click', previewButtonSelector, function (e) { $(document).on('click', previewButtonSelector, function(e) {
var $form; var $form;
e.preventDefault(); e.preventDefault();
$form = $(this).closest('form'); $form = $(this).closest('form');
$(document).triggerHandler('markdown-preview:show', [$form]); $(document).triggerHandler('markdown-preview:show', [$form]);
}); });
$(document).on('click', writeButtonSelector, function (e) { $(document).on('click', writeButtonSelector, function(e) {
var $form; var $form;
e.preventDefault(); e.preventDefault();
$form = $(this).closest('form'); $form = $(this).closest('form');

View file

@ -28,7 +28,7 @@ function keyCodeIs(e, keyCode) {
return e.keyCode === keyCode; return e.keyCode === keyCode;
} }
$(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { $(document).on('keydown.quick_submit', '.js-quick-submit', e => {
// Enter // Enter
if (!keyCodeIs(e, 13)) { if (!keyCodeIs(e, 13)) {
return; return;
@ -55,23 +55,25 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
// If the user tabs to a submit button on a `js-quick-submit` form, display a // If the user tabs to a submit button on a `js-quick-submit` form, display a
// tooltip to let them know they could've used the hotkey // tooltip to let them know they could've used the hotkey
$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function displayTooltip(e) { $(document).on(
// Tab 'keyup.quick_submit',
if (!keyCodeIs(e, 9)) { '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]',
return; function displayTooltip(e) {
} // Tab
if (!keyCodeIs(e, 9)) {
return;
}
const $this = $(this); const $this = $(this);
const title = isMac() ? const title = isMac() ? 'You can also press &#8984;-Enter' : 'You can also press Ctrl-Enter';
'You can also press &#8984;-Enter' :
'You can also press Ctrl-Enter';
$this.tooltip({ $this.tooltip({
container: 'body', container: 'body',
html: 'true', html: 'true',
placement: 'top', placement: 'top',
title, title,
trigger: 'manual', trigger: 'manual',
}); });
$this.tooltip('show').one('blur click', () => $this.tooltip('hide')); $this.tooltip('show').one('blur click', () => $this.tooltip('hide'));
}); },
);

View file

@ -18,7 +18,8 @@ import '../commons/bootstrap';
$.fn.requiresInput = function requiresInput() { $.fn.requiresInput = function requiresInput() {
const $form = $(this); const $form = $(this);
const $button = $('button[type=submit], input[type=submit]', $form); const $button = $('button[type=submit], input[type=submit]', $form);
const fieldSelector = 'input[required=required], select[required=required], textarea[required=required]'; const fieldSelector =
'input[required=required], select[required=required], textarea[required=required]';
function requireInput() { function requireInput() {
// Collect the input values of *all* required fields // Collect the input values of *all* required fields

View file

@ -32,16 +32,18 @@ export default class SecretValues {
updateDom(isRevealed) { updateDom(isRevealed) {
const values = this.container.querySelectorAll(this.valueSelector); const values = this.container.querySelectorAll(this.valueSelector);
values.forEach((value) => { values.forEach(value => {
value.classList.toggle('hide', !isRevealed); value.classList.toggle('hide', !isRevealed);
}); });
const placeholders = this.container.querySelectorAll(this.placeholderSelector); const placeholders = this.container.querySelectorAll(this.placeholderSelector);
placeholders.forEach((placeholder) => { placeholders.forEach(placeholder => {
placeholder.classList.toggle('hide', isRevealed); placeholder.classList.toggle('hide', isRevealed);
}); });
this.revealButton.textContent = isRevealed ? n__('Hide value', 'Hide values', values.length) : n__('Reveal value', 'Reveal values', values.length); this.revealButton.textContent = isRevealed
? n__('Hide value', 'Hide values', values.length)
: n__('Reveal value', 'Reveal values', values.length);
this.revealButton.dataset.secretRevealStatus = isRevealed; this.revealButton.dataset.secretRevealStatus = isRevealed;
} }
} }

View file

@ -88,22 +88,24 @@ export default class Shortcuts {
return null; return null;
} }
return axios.get(gon.shortcuts_path, { return axios
responseType: 'text', .get(gon.shortcuts_path, {
}).then(({ data }) => { responseType: 'text',
$.globalEval(data); })
.then(({ data }) => {
$.globalEval(data);
if (location && location.length > 0) { if (location && location.length > 0) {
const results = []; const results = [];
for (let i = 0, len = location.length; i < len; i += 1) { for (let i = 0, len = location.length; i < len; i += 1) {
results.push($(location[i]).show()); results.push($(location[i]).show());
}
return results;
} }
return results;
}
$('.hidden-shortcut').show(); $('.hidden-shortcut').show();
return $('.js-more-help-button').remove(); return $('.js-more-help-button').remove();
}); });
} }
focusFilter(e) { focusFilter(e) {

View file

@ -18,9 +18,7 @@ $(() => {
.toggleClass('fa-chevron-up', toggleState) .toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined); .toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
$container $container.find('.js-toggle-content').toggle(toggleState);
.find('.js-toggle-content')
.toggle(toggleState);
} }
$('body').on('click', '.js-toggle-button', function toggleButton(e) { $('body').on('click', '.js-toggle-button', function toggleButton(e) {

View file

@ -18,12 +18,7 @@ export default class Renderer {
this.loader = new STLLoader(); this.loader = new STLLoader();
this.fov = 45; this.fov = 45;
this.camera = new THREE.PerspectiveCamera( this.camera = new THREE.PerspectiveCamera(this.fov, this.width / this.height, 1, 1000);
this.fov,
this.width / this.height,
1,
1000,
);
this.scene = new THREE.Scene(); this.scene = new THREE.Scene();
@ -35,10 +30,7 @@ export default class Renderer {
this.setupLight(); this.setupLight();
// Set up OrbitControls // Set up OrbitControls
this.controls = new OrbitControls( this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.camera,
this.renderer.domElement,
);
this.controls.minDistance = 5; this.controls.minDistance = 5;
this.controls.maxDistance = 30; this.controls.maxDistance = 30;
this.controls.enableKeys = false; this.controls.enableKeys = false;
@ -51,47 +43,32 @@ export default class Renderer {
antialias: true, antialias: true,
}); });
this.renderer.setClearColor(0xFFFFFF); this.renderer.setClearColor(0xffffff);
this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize( this.renderer.setSize(this.width, this.height);
this.width,
this.height,
);
} }
setupLight() { setupLight() {
// Point light illuminates the object // Point light illuminates the object
const pointLight = new THREE.PointLight( const pointLight = new THREE.PointLight(0xffffff, 2, 0);
0xFFFFFF,
2,
0,
);
pointLight.castShadow = true; pointLight.castShadow = true;
this.camera.add(pointLight); this.camera.add(pointLight);
// Ambient light illuminates the scene // Ambient light illuminates the scene
const ambientLight = new THREE.AmbientLight( const ambientLight = new THREE.AmbientLight(0xffffff, 1);
0xFFFFFF,
1,
);
this.scene.add(ambientLight); this.scene.add(ambientLight);
} }
setupGrid() { setupGrid() {
this.grid = new THREE.GridHelper( this.grid = new THREE.GridHelper(20, 20, 0x000000, 0x000000);
20,
20,
0x000000,
0x000000,
);
this.scene.add(this.grid); this.scene.add(this.grid);
} }
loadFile() { loadFile() {
this.loader.load(this.container.dataset.endpoint, (geo) => { this.loader.load(this.container.dataset.endpoint, geo => {
const obj = new MeshObject(geo); const obj = new MeshObject(geo);
this.objects.push(obj); this.objects.push(obj);
@ -116,30 +93,23 @@ export default class Renderer {
} }
render() { render() {
this.renderer.render( this.renderer.render(this.scene, this.camera);
this.scene,
this.camera,
);
requestAnimationFrame(this.renderWrapper); requestAnimationFrame(this.renderWrapper);
} }
changeObjectMaterials(type) { changeObjectMaterials(type) {
this.objects.forEach((obj) => { this.objects.forEach(obj => {
obj.changeMaterial(type); obj.changeMaterial(type);
}); });
} }
setDefaultCameraPosition() { setDefaultCameraPosition() {
const obj = this.objects[0]; const obj = this.objects[0];
const radius = (obj.geometry.boundingSphere.radius / 1.5); const radius = obj.geometry.boundingSphere.radius / 1.5;
const dist = radius / (Math.sin((this.fov * (Math.PI / 180)) / 2)); const dist = radius / Math.sin((this.fov * (Math.PI / 180)) / 2);
this.camera.position.set( this.camera.position.set(0, dist + 1, dist);
0,
dist + 1,
dist,
);
this.camera.lookAt(this.grid); this.camera.lookAt(this.grid);
this.controls.update(); this.controls.update();

View file

@ -1,10 +1,6 @@
import { import { Matrix4, MeshLambertMaterial, Mesh } from 'three/build/three.module';
Matrix4,
MeshLambertMaterial,
Mesh,
} from 'three/build/three.module';
const defaultColor = 0xE24329; const defaultColor = 0xe24329;
const materials = { const materials = {
default: new MeshLambertMaterial({ default: new MeshLambertMaterial({
color: defaultColor, color: defaultColor,
@ -17,10 +13,7 @@ const materials = {
export default class MeshObject extends Mesh { export default class MeshObject extends Mesh {
constructor(geo) { constructor(geo) {
super( super(geo, materials.default);
geo,
materials.default,
);
this.geometry.computeBoundingSphere(); this.geometry.computeBoundingSphere();
@ -29,13 +22,7 @@ export default class MeshObject extends Mesh {
if (this.geometry.boundingSphere.radius > 4) { if (this.geometry.boundingSphere.radius > 4) {
const scale = 4 / this.geometry.boundingSphere.radius; const scale = 4 / this.geometry.boundingSphere.radius;
this.geometry.applyMatrix( this.geometry.applyMatrix(new Matrix4().makeScale(scale, scale, scale));
new Matrix4().makeScale(
scale,
scale,
scale,
),
);
this.geometry.computeBoundingSphere(); this.geometry.computeBoundingSphere();
this.position.x = -this.geometry.boundingSphere.center.x; this.position.x = -this.geometry.boundingSphere.center.x;

View file

@ -42,7 +42,7 @@ class BalsamiqViewer {
this.initDatabase(loadEvent.target.response); this.initDatabase(loadEvent.target.response);
const previews = this.getPreviews(); const previews = this.getPreviews();
previews.forEach((preview) => { previews.forEach(preview => {
const renderedPreview = this.renderPreview(preview); const renderedPreview = this.renderPreview(preview);
container.appendChild(renderedPreview); container.appendChild(renderedPreview);

View file

@ -41,39 +41,45 @@ export default class BlobFileDropzone {
addRemoveLinks: true, addRemoveLinks: true,
previewsContainer: '.dropzone-previews', previewsContainer: '.dropzone-previews',
headers: csrf.headers, headers: csrf.headers,
init: function () { init: function() {
this.on('addedfile', function () { this.on('addedfile', function() {
toggleLoading(submitButton, submitButtonLoadingIcon, false); toggleLoading(submitButton, submitButtonLoadingIcon, false);
dropzoneMessage.addClass(HIDDEN_CLASS); dropzoneMessage.addClass(HIDDEN_CLASS);
$('.dropzone-alerts').html('').hide(); $('.dropzone-alerts')
.html('')
.hide();
}); });
this.on('removedfile', function () { this.on('removedfile', function() {
toggleLoading(submitButton, submitButtonLoadingIcon, false); toggleLoading(submitButton, submitButtonLoadingIcon, false);
dropzoneMessage.removeClass(HIDDEN_CLASS); dropzoneMessage.removeClass(HIDDEN_CLASS);
}); });
this.on('success', function (header, response) { this.on('success', function(header, response) {
$('#modal-upload-blob').modal('hide'); $('#modal-upload-blob').modal('hide');
visitUrl(response.filePath); visitUrl(response.filePath);
}); });
this.on('maxfilesexceeded', function (file) { this.on('maxfilesexceeded', function(file) {
dropzoneMessage.addClass(HIDDEN_CLASS); dropzoneMessage.addClass(HIDDEN_CLASS);
this.removeFile(file); this.removeFile(file);
}); });
this.on('sending', function (file, xhr, formData) { this.on('sending', function(file, xhr, formData) {
formData.append('branch_name', form.find('.js-branch-name').val()); formData.append('branch_name', form.find('.js-branch-name').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val()); formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val()); formData.append('commit_message', form.find('.js-commit-message').val());
}); });
}, },
// Override behavior of adding error underneath preview // Override behavior of adding error underneath preview
error: function (file, errorMessage) { error: function(file, errorMessage) {
const stripped = $('<div/>').html(errorMessage).text(); const stripped = $('<div/>')
$('.dropzone-alerts').html(`Error uploading file: "${stripped}"`).show(); .html(errorMessage)
.text();
$('.dropzone-alerts')
.html(`Error uploading file: "${stripped}"`)
.show();
this.removeFile(file); this.removeFile(file);
}, },
}); });
submitButton.on('click', (e) => { submitButton.on('click', e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (dropzone[0].dropzone.getQueuedFiles().length === 0) { if (dropzone[0].dropzone.getQueuedFiles().length === 0) {

View file

@ -2,17 +2,19 @@ import { getLocationHash } from '../lib/utils/url_utility';
const lineNumberRe = /^L[0-9]+/; const lineNumberRe = /^L[0-9]+/;
const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => { const updateLineNumbersOnBlobPermalinks = linksToUpdate => {
const hash = getLocationHash(); const hash = getLocationHash();
if (hash && lineNumberRe.test(hash)) { if (hash && lineNumberRe.test(hash)) {
const hashUrlString = `#${hash}`; const hashUrlString = `#${hash}`;
[].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => { [].concat(Array.prototype.slice.call(linksToUpdate)).forEach(permalinkButton => {
const baseHref = permalinkButton.getAttribute('data-original-href') || (() => { const baseHref =
const href = permalinkButton.getAttribute('href'); permalinkButton.getAttribute('data-original-href') ||
permalinkButton.setAttribute('data-original-href', href); (() => {
return href; const href = permalinkButton.getAttribute('href');
})(); permalinkButton.setAttribute('data-original-href', href);
return href;
})();
permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`); permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);
}); });
} }
@ -26,7 +28,7 @@ function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, element
}, 0); }, 0);
}; };
blobContentHolder.addEventListener('click', (e) => { blobContentHolder.addEventListener('click', e => {
if (e.target.matches(lineNumberSelector)) { if (e.target.matches(lineNumberSelector)) {
updateBlameAndBlobPermalinkCb(); updateBlameAndBlobPermalinkCb();
} }

View file

@ -45,15 +45,11 @@ export default class FileTemplateSelector {
} }
renderLoading() { renderLoading() {
this.$loadingIcon this.$loadingIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down');
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
} }
renderLoaded() { renderLoaded() {
this.$loadingIcon this.$loadingIcon.addClass('fa-chevron-down').removeClass('fa-spinner fa-spin');
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
} }
reportSelection(options) { reportSelection(options) {

View file

@ -40,13 +40,14 @@ export default () => {
}, },
methods: { methods: {
loadFile() { loadFile() {
axios.get(el.dataset.endpoint) axios
.get(el.dataset.endpoint)
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
this.json = data; this.json = data;
this.loading = false; this.loading = false;
}) })
.catch((e) => { .catch(e => {
if (e.status !== 200) { if (e.status !== 200) {
this.loadError = true; this.loadError = true;
} }

View file

@ -13,7 +13,7 @@ export default class SketchLoader {
return this.getZipFile() return this.getZipFile()
.then(data => JSZip.loadAsync(data)) .then(data => JSZip.loadAsync(data))
.then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array')) .then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array'))
.then((content) => { .then(content => {
const url = window.URL || window.webkitURL; const url = window.URL || window.webkitURL;
const blob = new Blob([new Uint8Array(content)], { const blob = new Blob([new Uint8Array(content)], {
type: 'image/png', type: 'image/png',

View file

@ -3,8 +3,8 @@ import Renderer from './3d_viewer';
export default () => { export default () => {
const viewer = new Renderer(document.getElementById('js-stl-viewer')); const viewer = new Renderer(document.getElementById('js-stl-viewer'));
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => { [].slice.call(document.querySelectorAll('.js-material-changer')).forEach(el => {
el.addEventListener('click', (e) => { el.addEventListener('click', e => {
const { target } = e; const { target } = e;
e.preventDefault(); e.preventDefault();

View file

@ -81,14 +81,10 @@ export default class TemplateSelector {
} }
startLoadingSpinner() { startLoadingSpinner() {
this.$dropdownIcon this.$dropdownIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down');
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
} }
stopLoadingSpinner() { stopLoadingSpinner() {
this.$dropdownIcon this.$dropdownIcon.addClass('fa-chevron-down').removeClass('fa-spinner fa-spin');
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
} }
} }

View file

@ -22,7 +22,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
search: { search: {
fields: ['name'], fields: ['name'],
}, },
clicked: (options) => { clicked: options => {
const { e } = options; const { e } = options;
const el = options.$el; const el = options.$el;
const query = options.selectedObj; const query = options.selectedObj;

View file

@ -21,5 +21,4 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector {
text: item => item.name, text: item => item.name,
}); });
} }
} }

View file

@ -22,9 +22,8 @@ export default class BlobViewer {
const viewer = document.querySelector('.blob-viewer[data-type="rich"]'); const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
if (!viewer || !viewer.dataset.richType) return; if (!viewer || !viewer.dataset.richType) return;
const initViewer = promise => promise const initViewer = promise =>
.then(module => module.default(viewer)) promise.then(module => module.default(viewer)).catch(error => {
.catch((error) => {
Flash('Error loading file viewer.'); Flash('Error loading file viewer.');
throw error; throw error;
}); });
@ -79,10 +78,9 @@ export default class BlobViewer {
initBindings() { initBindings() {
if (this.switcherBtns.length) { if (this.switcherBtns.length) {
Array.from(this.switcherBtns) Array.from(this.switcherBtns).forEach(el => {
.forEach((el) => { el.addEventListener('click', this.switchViewHandler.bind(this));
el.addEventListener('click', this.switchViewHandler.bind(this)); });
});
} }
if (this.copySourceBtn) { if (this.copySourceBtn) {
@ -109,7 +107,10 @@ export default class BlobViewer {
this.copySourceBtn.setAttribute('title', 'Copy source to clipboard'); this.copySourceBtn.setAttribute('title', 'Copy source to clipboard');
this.copySourceBtn.classList.remove('disabled'); this.copySourceBtn.classList.remove('disabled');
} else if (this.activeViewer === this.simpleViewer) { } else if (this.activeViewer === this.simpleViewer) {
this.copySourceBtn.setAttribute('title', 'Wait for the source to load to copy it to the clipboard'); this.copySourceBtn.setAttribute(
'title',
'Wait for the source to load to copy it to the clipboard',
);
this.copySourceBtn.classList.add('disabled'); this.copySourceBtn.classList.add('disabled');
} else { } else {
this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard'); this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard');
@ -147,15 +148,15 @@ export default class BlobViewer {
this.toggleCopyButtonState(); this.toggleCopyButtonState();
BlobViewer.loadViewer(newViewer) BlobViewer.loadViewer(newViewer)
.then((viewer) => { .then(viewer => {
$(viewer).renderGFM(); $(viewer).renderGFM();
this.$fileHolder.trigger('highlight:line'); this.$fileHolder.trigger('highlight:line');
handleLocationHash(); handleLocationHash();
this.toggleCopyButtonState(); this.toggleCopyButtonState();
}) })
.catch(() => new Flash('Error loading viewer')); .catch(() => new Flash('Error loading viewer'));
} }
static loadViewer(viewerParam) { static loadViewer(viewerParam) {
@ -168,12 +169,11 @@ export default class BlobViewer {
viewer.setAttribute('data-loading', 'true'); viewer.setAttribute('data-loading', 'true');
return axios.get(url) return axios.get(url).then(({ data }) => {
.then(({ data }) => { viewer.innerHTML = data.html;
viewer.innerHTML = data.html; viewer.setAttribute('data-loaded', 'true');
viewer.setAttribute('data-loaded', 'true');
return viewer; return viewer;
}); });
} }
} }

View file

@ -13,11 +13,11 @@ export default () => {
if (editBlobForm.length) { if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot'); const urlRoot = editBlobForm.data('relativeUrlRoot');
const assetsPath = editBlobForm.data('assetsPrefix'); const assetsPath = editBlobForm.data('assetsPrefix');
const blobLanguage = editBlobForm.data('blobLanguage'); const filePath = editBlobForm.data('blobFilename');
const currentAction = $('.js-file-title').data('currentAction'); const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id'); const projectId = editBlobForm.data('project-id');
new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction, projectId); new EditBlob(`${urlRoot}${assetsPath}`, filePath, currentAction, projectId);
new NewCommitForm(editBlobForm); new NewCommitForm(editBlobForm);
} }

View file

@ -5,6 +5,7 @@ import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator'; import TemplateSelectorMediator from '../blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils';
export default class EditBlob { export default class EditBlob {
constructor(assetsPath, aceMode, currentAction, projectId) { constructor(assetsPath, aceMode, currentAction, projectId) {
@ -14,9 +15,10 @@ export default class EditBlob {
this.initFileSelectors(currentAction, projectId); this.initFileSelectors(currentAction, projectId);
} }
configureAceEditor(aceMode, assetsPath) { configureAceEditor(filePath, assetsPath) {
ace.config.set('modePath', `${assetsPath}/ace`); ace.config.set('modePath', `${assetsPath}/ace`);
ace.config.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/searchbox');
ace.config.loadModule('ace/ext/modelist');
this.editor = ace.edit('editor'); this.editor = ace.edit('editor');
@ -25,8 +27,8 @@ export default class EditBlob {
this.editor.focus(); this.editor.focus();
if (aceMode) { if (filePath) {
this.editor.getSession().setMode(`ace/mode/${aceMode}`); this.editor.getSession().setMode(getModeByFileExtension(filePath));
} }
} }

View file

@ -1,25 +1,20 @@
/* eslint-disable comma-dangle */
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import Vue from 'vue'; import Vue from 'vue';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip'; import Tooltip from '~/vue_shared/directives/tooltip';
import AccessorUtilities from '../../lib/utils/accessor'; import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue';
import BoardBlankState from './board_blank_state.vue'; import BoardBlankState from './board_blank_state.vue';
import './board_delete'; import BoardDelete from './board_delete';
import BoardList from './board_list.vue';
import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
const Store = gl.issueBoards.BoardsStore; export default Vue.extend({
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({
components: { components: {
boardList,
'board-delete': gl.issueBoards.BoardDelete,
BoardBlankState, BoardBlankState,
BoardDelete,
BoardList,
Icon, Icon,
}, },
directives: { directives: {
@ -47,10 +42,10 @@ gl.issueBoards.Board = Vue.extend({
required: true, required: true,
}, },
}, },
data () { data() {
return { return {
detailIssue: Store.detail, detailIssue: boardsStore.detail,
filter: Store.filter, filter: boardsStore.filter,
}; };
}, },
computed: { computed: {
@ -58,44 +53,47 @@ gl.issueBoards.Board = Vue.extend({
const { issuesSize } = this.list; const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`; return `${n__('%d issue', '%d issues', issuesSize)}`;
}, },
isNewIssueShown() {
return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed');
},
}, },
watch: { watch: {
filter: { filter: {
handler() { handler() {
this.list.page = 1; this.list.page = 1;
this.list.getIssues(true) this.list.getIssues(true).catch(() => {
.catch(() => { // TODO: handle request error
// TODO: handle request error });
});
}, },
deep: true, deep: true,
} },
}, },
mounted () { mounted() {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ this.sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
group: 'boards', group: 'boards',
draggable: '.is-draggable', draggable: '.is-draggable',
handle: '.js-board-handle', handle: '.js-board-handle',
onEnd: (e) => { onEnd: e => {
gl.issueBoards.onEnd(); sortableEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray(); const order = this.sortable.toArray();
const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10));
this.$nextTick(() => { this.$nextTick(() => {
Store.moveList(list, order); boardsStore.moveList(list, order);
}); });
} }
} },
}); });
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
}, },
created() { created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) { if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false'; const isCollapsed =
localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed; this.list.isExpanded = !isCollapsed;
} }
@ -109,7 +107,10 @@ gl.issueBoards.Board = Vue.extend({
this.list.isExpanded = !this.list.isExpanded; this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) { if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); localStorage.setItem(
`boards.${this.boardId}.${this.list.type}.expanded`,
this.list.isExpanded,
);
} }
} }
}, },

View file

@ -2,8 +2,7 @@
/* global ListLabel */ /* global ListLabel */
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import boardsStore from '../stores/boards_store';
const Store = gl.issueBoards.BoardsStore;
export default { export default {
data() { data() {
@ -19,7 +18,7 @@ export default {
this.clearBlankState(); this.clearBlankState();
this.predefinedLabels.forEach((label, i) => { this.predefinedLabels.forEach((label, i) => {
Store.addList({ boardsStore.addList({
title: label.title, title: label.title,
position: i, position: i,
list_type: 'label', list_type: 'label',
@ -30,35 +29,34 @@ export default {
}); });
}); });
Store.state.lists = _.sortBy(Store.state.lists, 'position'); boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position');
// Save the labels // Save the labels
gl.boardService.generateDefaultLists() gl.boardService
.generateDefaultLists()
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
data.forEach((listObj) => { data.forEach(listObj => {
const list = Store.findList('title', listObj.title); const list = boardsStore.findList('title', listObj.title);
list.id = listObj.id; list.id = listObj.id;
list.label.id = listObj.label.id; list.label.id = listObj.label.id;
list.getIssues() list.getIssues().catch(() => {
.catch(() => { // TODO: handle request error
// TODO: handle request error });
});
}); });
}) })
.catch(() => { .catch(() => {
Store.removeList(undefined, 'label'); boardsStore.removeList(undefined, 'label');
Cookies.remove('issue_board_welcome_hidden', { Cookies.remove('issue_board_welcome_hidden', {
path: '', path: '',
}); });
Store.addBlankState(); boardsStore.addBlankState();
}); });
}, },
clearBlankState: Store.removeBlankState.bind(Store), clearBlankState: boardsStore.removeBlankState.bind(boardsStore),
}, },
}; };
</script> </script>
<template> <template>

View file

@ -1,78 +1,77 @@
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue'; import IssueCardInner from './issue_card_inner.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
const Store = gl.issueBoards.BoardsStore; export default {
name: 'BoardsIssueCard',
components: {
IssueCardInner,
},
props: {
list: {
type: Object,
default: () => ({}),
},
issue: {
type: Object,
default: () => ({}),
},
issueLinkBase: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
index: {
type: Number,
default: 0,
},
rootPath: {
type: String,
default: '',
},
groupId: {
type: Number,
},
},
data() {
return {
showDetail: false,
detailIssue: boardsStore.detail,
};
},
computed: {
issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
},
},
methods: {
mouseDown() {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
export default { if (this.showDetail) {
name: 'BoardsIssueCard',
components: {
IssueCardInner,
},
props: {
list: {
type: Object,
default: () => ({}),
},
issue: {
type: Object,
default: () => ({}),
},
issueLinkBase: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
index: {
type: Number,
default: 0,
},
rootPath: {
type: String,
default: '',
},
groupId: {
type: Number,
},
},
data() {
return {
showDetail: false,
detailIssue: Store.detail,
};
},
computed: {
issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
},
},
methods: {
mouseDown() {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false; this.showDetail = false;
},
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) { if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
this.showDetail = false; eventHub.$emit('clearDetailIssue');
} else {
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { eventHub.$emit('newDetailIssue', this.issue);
eventHub.$emit('clearDetailIssue'); boardsStore.detail.list = this.list;
} else {
eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list;
}
} }
}, }
}, },
}; },
};
</script> </script>
<template> <template>

View file

@ -1,12 +1,7 @@
/* eslint-disable comma-dangle, no-alert */
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
window.gl = window.gl || {}; export default Vue.extend({
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardDelete = Vue.extend({
props: { props: {
list: { list: {
type: Object, type: Object,
@ -14,12 +9,13 @@ gl.issueBoards.BoardDelete = Vue.extend({
}, },
}, },
methods: { methods: {
deleteBoard () { deleteBoard() {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to delete this list?')) { if (window.confirm('Are you sure you want to delete this list?')) {
this.list.destroy(); this.list.destroy();
} }
} },
} },
}); });

View file

@ -1,16 +1,18 @@
<script> <script>
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import boardNewIssue from './board_new_issue.vue'; import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue'; import boardCard from './board_card.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
const Store = gl.issueBoards.BoardsStore; import { getBoardSortableDefaultOptions, sortableStart } from '../mixins/sortable_default_options';
export default { export default {
name: 'BoardList', name: 'BoardList',
components: { components: {
boardCard, boardCard,
boardNewIssue, boardNewIssue,
GlLoadingIcon,
}, },
props: { props: {
groupId: { groupId: {
@ -46,7 +48,7 @@ export default {
data() { data() {
return { return {
scrollOffset: 250, scrollOffset: 250,
filters: Store.state.filters, filters: boardsStore.state.filters,
showCount: false, showCount: false,
showIssueForm: false, showIssueForm: false,
}; };
@ -61,13 +63,14 @@ export default {
}, },
issues() { issues() {
this.$nextTick(() => { this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() && if (
this.list.issuesSize > this.list.issues.length) { this.scrollHeight() <= this.listHeight() &&
this.list.issuesSize > this.list.issues.length
) {
this.list.page += 1; this.list.page += 1;
this.list.getIssues(false) this.list.getIssues(false).catch(() => {
.catch(() => { // TODO: handle request error
// TODO: handle request error });
});
} }
if (this.scrollHeight() > Math.ceil(this.listHeight())) { if (this.scrollHeight() > Math.ceil(this.listHeight())) {
@ -83,7 +86,7 @@ export default {
eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
}, },
mounted() { mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = getBoardSortableDefaultOptions({
scroll: true, scroll: true,
disabled: this.disabled, disabled: this.disabled,
filter: '.board-list-count, .is-disabled', filter: '.board-list-count, .is-disabled',
@ -108,7 +111,8 @@ export default {
// So from there, we can get reference to actual container // So from there, we can get reference to actual container
// and thus the container type to enable Copy or Move // and thus the container type to enable Copy or Move
if (e.target) { if (e.target) {
const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); const containerEl =
e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
const toBoardType = containerEl.dataset.boardType; const toBoardType = containerEl.dataset.boardType;
const cloneActions = { const cloneActions = {
label: ['milestone', 'assignee'], label: ['milestone', 'assignee'],
@ -120,8 +124,9 @@ export default {
const fromBoardType = this.list.type; const fromBoardType = this.list.type;
// For each list we check if the destination list is // For each list we check if the destination list is
// a the list were we should clone the issue // a the list were we should clone the issue
const shouldClone = Object.entries(cloneActions).some(entry => ( const shouldClone = Object.entries(cloneActions).some(
fromBoardType === entry[0] && entry[1].includes(toBoardType))); entry => fromBoardType === entry[0] && entry[1].includes(toBoardType),
);
if (shouldClone) { if (shouldClone) {
return 'clone'; return 'clone';
@ -133,28 +138,36 @@ export default {
}, },
revertClone: true, revertClone: true,
}, },
onStart: (e) => { onStart: e => {
const card = this.$refs.issue[e.oldIndex]; const card = this.$refs.issue[e.oldIndex];
card.showDetail = false; card.showDetail = false;
Store.moving.list = card.list; boardsStore.moving.list = card.list;
Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); boardsStore.moving.issue = boardsStore.moving.list.findIssue(+e.item.dataset.issueId);
gl.issueBoards.onStart(); sortableStart();
}, },
onAdd: (e) => { onAdd: e => {
gl.issueBoards.BoardsStore boardsStore.moveIssueToList(
.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); boardsStore.moving.list,
this.list,
boardsStore.moving.issue,
e.newIndex,
);
this.$nextTick(() => { this.$nextTick(() => {
e.item.remove(); e.item.remove();
}); });
}, },
onUpdate: (e) => { onUpdate: e => {
const sortedArray = this.sortable.toArray() const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
.filter(id => id !== '-1'); boardsStore.moveIssueInList(
gl.issueBoards.BoardsStore this.list,
.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); boardsStore.moving.issue,
e.oldIndex,
e.newIndex,
sortedArray,
);
}, },
onMove(e) { onMove(e) {
return !e.related.classList.contains('board-list-count'); return !e.related.classList.contains('board-list-count');
@ -192,16 +205,14 @@ export default {
if (getIssues) { if (getIssues) {
this.list.loadingMore = true; this.list.loadingMore = true;
getIssues getIssues.then(loadingDone).catch(loadingDone);
.then(loadingDone)
.catch(loadingDone);
} }
}, },
toggleForm() { toggleForm() {
this.showIssueForm = !this.showIssueForm; this.showIssueForm = !this.showIssueForm;
}, },
onScroll() { onScroll() {
if (!this.list.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) {
this.loadNextPage(); this.loadNextPage();
} }
}, },

View file

@ -1,17 +1,16 @@
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { Button } from '@gitlab-org/gitlab-ui'; import { GlButton } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue'; import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue'; import ListIssue from '../models/issue';
import boardsStore from '../stores/boards_store';
const Store = gl.issueBoards.BoardsStore;
export default { export default {
name: 'BoardNewIssue', name: 'BoardNewIssue',
components: { components: {
ProjectSelect, ProjectSelect,
'gl-button': Button, GlButton,
}, },
props: { props: {
groupId: { groupId: {
@ -63,13 +62,14 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`); eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel(); this.cancel();
return this.list.newIssue(issue) return this.list
.newIssue(issue)
.then(() => { .then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
Store.detail.issue = issue; boardsStore.detail.issue = issue;
Store.detail.list = this.list; boardsStore.detail.list = this.list;
}) })
.catch(() => { .catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions

View file

@ -1,4 +1,4 @@
/* eslint-disable comma-dangle, no-new */ /* eslint-disable no-new */
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
@ -14,13 +14,9 @@ import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select'; import LabelsSelect from '../../labels_select';
import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import MilestoneSelect from '../../milestone_select'; import MilestoneSelect from '../../milestone_select';
import boardsStore from '../stores/boards_store';
const Store = gl.issueBoards.BoardsStore; export default Vue.extend({
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({
components: { components: {
AssigneeTitle, AssigneeTitle,
Assignees, Assignees,
@ -35,14 +31,14 @@ gl.issueBoards.BoardSidebar = Vue.extend({
}, },
data() { data() {
return { return {
detail: Store.detail, detail: boardsStore.detail,
issue: {}, issue: {},
list: {}, list: {},
loadingAssignees: false, loadingAssignees: false,
}; };
}, },
computed: { computed: {
showSidebar () { showSidebar() {
return Object.keys(this.issue).length; return Object.keys(this.issue).length;
}, },
milestoneTitle() { milestoneTitle() {
@ -55,18 +51,20 @@ gl.issueBoards.BoardSidebar = Vue.extend({
return this.issue.labels && this.issue.labels.length; return this.issue.labels && this.issue.labels.length;
}, },
labelDropdownTitle() { labelDropdownTitle() {
return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), { return this.hasLabels
firstLabel: this.issue.labels[0].title, ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
labelCount: this.issue.labels.length - 1 firstLabel: this.issue.labels[0].title,
}) : __('Label'); labelCount: this.issue.labels.length - 1,
})
: __('Label');
}, },
selectedLabels() { selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
} },
}, },
watch: { watch: {
detail: { detail: {
handler () { handler() {
if (this.issue.id !== this.detail.issue.id) { if (this.issue.id !== this.detail.issue.id) {
$('.block.assignee') $('.block.assignee')
.find('input:not(.js-vue)[name="issue[assignee_ids][]"]') .find('input:not(.js-vue)[name="issue[assignee_ids][]"]')
@ -75,17 +73,19 @@ gl.issueBoards.BoardSidebar = Vue.extend({
}); });
$('.js-issue-board-sidebar', this.$el).each((i, el) => { $('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el).data('glDropdown').clearMenu(); $(el)
.data('glDropdown')
.clearMenu();
}); });
} }
this.issue = this.detail.issue; this.issue = this.detail.issue;
this.list = this.detail.list; this.list = this.detail.list;
}, },
deep: true deep: true,
}, },
}, },
created () { created() {
// Get events from glDropdown // Get events from glDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee); eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee); eventHub.$on('sidebar.addAssignee', this.addAssignee);
@ -98,7 +98,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees); eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
}, },
mounted () { mounted() {
new IssuableContext(this.currentUser); new IssuableContext(this.currentUser);
new MilestoneSelect(); new MilestoneSelect();
new DueDateSelectors(); new DueDateSelectors();
@ -106,29 +106,30 @@ gl.issueBoards.BoardSidebar = Vue.extend({
new Sidebar(); new Sidebar();
}, },
methods: { methods: {
closeSidebar () { closeSidebar() {
this.detail.issue = {}; this.detail.issue = {};
}, },
assignSelf () { assignSelf() {
// Notify gl dropdown that we are now assigning to current user // Notify gl dropdown that we are now assigning to current user
this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself')); this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself'));
this.addAssignee(this.currentUser); this.addAssignee(this.currentUser);
this.saveAssignees(); this.saveAssignees();
}, },
removeAssignee (a) { removeAssignee(a) {
gl.issueBoards.BoardsStore.detail.issue.removeAssignee(a); boardsStore.detail.issue.removeAssignee(a);
}, },
addAssignee (a) { addAssignee(a) {
gl.issueBoards.BoardsStore.detail.issue.addAssignee(a); boardsStore.detail.issue.addAssignee(a);
}, },
removeAllAssignees () { removeAllAssignees() {
gl.issueBoards.BoardsStore.detail.issue.removeAllAssignees(); boardsStore.detail.issue.removeAllAssignees();
}, },
saveAssignees () { saveAssignees() {
this.loadingAssignees = true; this.loadingAssignees = true;
gl.issueBoards.BoardsStore.detail.issue.update() boardsStore.detail.issue
.update()
.then(() => { .then(() => {
this.loadingAssignees = false; this.loadingAssignees = false;
}) })

View file

@ -1,163 +1,222 @@
<script> <script>
import $ from 'jquery'; import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import { sprintf, __ } from '~/locale';
import eventHub from '../eventhub'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store';
const Store = gl.issueBoards.BoardsStore; export default {
components: {
Icon,
UserAvatarLink,
TooltipOnTruncate,
IssueDueDate,
IssueTimeEstimate,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
issue: {
type: Object,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
list: {
type: Object,
required: false,
default: () => ({}),
},
rootPath: {
type: String,
required: true,
},
updateFilters: {
type: Boolean,
required: false,
default: false,
},
groupId: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
limitBeforeCounter: 2,
maxRender: 3,
maxCounter: 99,
};
},
computed: {
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
const { numberOverLimit, maxCounter } = this;
const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit;
return sprintf(__('%{count} more assignees'), { count });
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
return `${this.maxCounter}+`;
}
export default { return `+${this.numberOverLimit}`;
components: {
UserAvatarLink,
}, },
directives: { shouldRenderCounter() {
tooltip, if (this.issue.assignees.length <= this.maxRender) {
return false;
}
return this.issue.assignees.length > this.numberOverLimit;
}, },
props: { issueId() {
issue: { if (this.issue.iid) {
type: Object, return `#${this.issue.iid}`;
required: true, }
}, return false;
issueLinkBase: {
type: String,
required: true,
},
list: {
type: Object,
required: false,
default: () => ({}),
},
rootPath: {
type: String,
required: true,
},
updateFilters: {
type: Boolean,
required: false,
default: false,
},
groupId: {
type: Number,
required: false,
default: null,
},
}, },
data() { showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
issueReferencePath() {
const { referencePath, groupId } = this.issue;
return !groupId ? referencePath.split('#')[0] : null;
},
},
methods: {
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
if (!assignee) return '';
return `${this.rootPath}${assignee.username}`;
},
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
if (!label.id) return false;
return true;
},
filterByLabel(label) {
if (!this.updateFilters) return;
const labelTitle = encodeURIComponent(label.title);
const filter = `label_name[]=${labelTitle}`;
this.applyFilter(filter);
},
filterByWeight(weight) {
if (!this.updateFilters) return;
const issueWeight = encodeURIComponent(weight);
const filter = `weight=${issueWeight}`;
this.applyFilter(filter);
},
applyFilter(filter) {
const filterPath = boardsStore.filter.path.split('&');
const filterIndex = filterPath.indexOf(filter);
if (filterIndex === -1) {
filterPath.push(filter);
} else {
filterPath.splice(filterIndex, 1);
}
boardsStore.filter.path = filterPath.join('&');
boardsStore.updateFiltersUrl();
eventHub.$emit('updateTokens');
},
labelStyle(label) {
return { return {
limitBeforeCounter: 3, backgroundColor: label.color,
maxRender: 4, color: label.textColor,
maxCounter: 99,
}; };
}, },
computed: { },
numberOverLimit() { };
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
return `${this.assigneeCounterLabel} more`;
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
return `${this.maxCounter}+`;
}
return `+${this.numberOverLimit}`;
},
shouldRenderCounter() {
if (this.issue.assignees.length <= this.maxRender) {
return false;
}
return this.issue.assignees.length > this.numberOverLimit;
},
issueId() {
if (this.issue.iid) {
return `#${this.issue.iid}`;
}
return false;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
},
methods: {
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
return `${this.rootPath}${assignee.username}`;
},
assigneeUrlTitle(assignee) {
return `Assigned to ${assignee.name}`;
},
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
if (!label.id) return false;
return true;
},
filterByLabel(label, e) {
if (!this.updateFilters) return;
const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param);
$(e.currentTarget).tooltip('hide');
if (labelIndex === -1) {
filterPath.push(param);
} else {
filterPath.splice(labelIndex, 1);
}
gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
Store.updateFiltersUrl();
eventHub.$emit('updateTokens');
},
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
},
};
</script> </script>
<template> <template>
<div> <div>
<div class="board-card-header"> <div class="board-card-header">
<h4 class="board-card-title"> <h4 class="board-card-title append-bottom-0 prepend-top-0">
<i <icon
v-if="issue.confidential" v-if="issue.confidential"
class="fa fa-eye-slash confidential-icon" v-gl-tooltip
aria-hidden="true" name="eye-slash"
></i> :title="__('Confidential')"
<a class="confidential-icon append-right-4"
:aria-label="__('Confidential')"
/><a
:href="issue.path" :href="issue.path"
:title="issue.title" :title="issue.title"
class="js-no-trigger" class="js-no-trigger"
@mousemove.stop>{{ issue.title }}</a> @mousemove.stop>{{ issue.title }}</a>
<span
v-if="issueId"
class="board-card-number append-right-5"
>
{{ issue.referencePath }}
</span>
</h4> </h4>
</div>
<div
v-if="showLabelFooter"
class="board-card-labels prepend-top-4 d-flex flex-wrap"
>
<button
v-for="label in issue.labels"
v-if="showLabel(label)"
:key="label.id"
v-gl-tooltip
:style="labelStyle(label)"
:title="label.description"
class="badge color-label append-right-4 prepend-top-4"
type="button"
@click="filterByLabel(label)"
>
{{ label.title }}
</button>
</div>
<div class="board-card-footer d-flex justify-content-between align-items-end">
<div class="d-flex align-items-start flex-wrap-reverse board-card-number-container js-board-card-number-container">
<span
v-if="issue.referencePath"
class="board-card-number d-flex append-right-8 prepend-top-8"
>
<tooltip-on-truncate
v-if="issueReferencePath"
:title="issueReferencePath"
placement="bottom"
class="board-issue-path block-truncated bold"
>{{ issueReferencePath }}</tooltip-on-truncate>#{{ issue.iid }}
</span>
<span class="board-info-items prepend-top-8 d-inline-block">
<issue-due-date
v-if="issue.dueDate"
:date="issue.dueDate"
/><issue-time-estimate
v-if="issue.timeEstimate"
:estimate="issue.timeEstimate"
/>
</span>
</div>
<div class="board-card-assignee"> <div class="board-card-assignee">
<user-avatar-link <user-avatar-link
v-for="(assignee, index) in issue.assignees" v-for="(assignee, index) in issue.assignees"
@ -166,38 +225,26 @@
:link-href="assigneeUrl(assignee)" :link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)" :img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatar" :img-src="assignee.avatar"
:tooltip-text="assigneeUrlTitle(assignee)" :img-size="24"
class="js-no-trigger" class="js-no-trigger"
tooltip-placement="bottom" tooltip-placement="bottom"
/> >
<span class="js-assignee-tooltip">
<span class="bold d-block">Assignee</span>
{{ assignee.name }}
<span class="text-white-50">@{{ assignee.username }}</span>
</span>
</user-avatar-link>
<span <span
v-if="shouldRenderCounter" v-if="shouldRenderCounter"
v-tooltip v-gl-tooltip
:title="assigneeCounterTooltip" :title="assigneeCounterTooltip"
class="avatar-counter" class="avatar-counter"
data-placement="bottom"
> >
{{ assigneeCounterLabel }} {{ assigneeCounterLabel }}
</span> </span>
</div> </div>
</div> </div>
<div
v-if="showLabelFooter"
class="board-card-footer"
>
<button
v-for="label in issue.labels"
v-if="showLabel(label)"
:key="label.id"
v-tooltip
:style="labelStyle(label)"
:title="label.description"
class="badge color-label"
type="button"
data-container="body"
@click="filterByLabel(label, $event)"
>
{{ label.title }}
</button>
</div>
</div> </div>
</template> </template>

View file

@ -0,0 +1,90 @@
<script>
import dateFormat from 'dateformat';
import { GlTooltip } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import { getDayDifference, getTimeago, dateInWords } from '~/lib/utils/datetime_utility';
export default {
components: {
Icon,
GlTooltip,
},
props: {
date: {
type: String,
required: true,
},
},
computed: {
title() {
const timeago = getTimeago();
const { timeDifference, standardDateFormat } = this;
const formatedDate = standardDateFormat;
if (timeDifference >= -1 && timeDifference < 7) {
return `${timeago.format(this.issueDueDate)} (${formatedDate})`;
}
return timeago.format(this.issueDueDate);
},
body() {
const { timeDifference, issueDueDate, standardDateFormat } = this;
if (timeDifference === 0) {
return __('Today');
} else if (timeDifference === 1) {
return __('Tomorrow');
} else if (timeDifference === -1) {
return __('Yesterday');
} else if (timeDifference > 0 && timeDifference < 7) {
return dateFormat(issueDueDate, 'dddd', true);
}
return standardDateFormat;
},
issueDueDate() {
return new Date(this.date);
},
timeDifference() {
const today = new Date();
return getDayDifference(today, this.issueDueDate);
},
isPastDue() {
if (this.timeDifference >= 0) return false;
return true;
},
standardDateFormat() {
const today = new Date();
const isDueInCurrentYear = today.getFullYear() === this.issueDueDate.getFullYear();
return dateInWords(this.issueDueDate, true, isDueInCurrentYear);
},
},
};
</script>
<template>
<span>
<span
ref="issueDueDate"
class="board-card-info card-number"
>
<icon
:class="{'text-danger': isPastDue, 'board-card-info-icon': true}"
name="calendar"
/><time
:class="{'text-danger': isPastDue}"
datetime="date"
class="board-card-info-text">{{ body }}</time>
</span>
<gl-tooltip
:target="() => $refs.issueDueDate"
placement="bottom"
>
<span class="bold">{{ __('Due date') }}</span>
<br />
<span :class="{'text-danger-muted': isPastDue}">{{ title }}</span>
</gl-tooltip>
</span>
</template>

View file

@ -0,0 +1,48 @@
<script>
import { GlTooltip } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
export default {
components: {
Icon,
GlTooltip,
},
props: {
estimate: {
type: Number,
required: true,
},
},
computed: {
title() {
return stringifyTime(parseSeconds(this.estimate), true);
},
timeEstimate() {
return stringifyTime(parseSeconds(this.estimate));
},
},
};
</script>
<template>
<span>
<span
ref="issueTimeEstimate"
class="board-card-info card-number"
>
<icon
name="hourglass"
css-classes="board-card-info-icon"
/><time class="board-card-info-text">{{ timeEstimate }}</time>
</span>
<gl-tooltip
:target="() => $refs.issueTimeEstimate"
placement="bottom"
class="js-issue-time-estimate"
>
<span class="bold d-block">{{ __('Time estimate') }}</span>
{{ title }}
</gl-tooltip>
</span>
</template>

View file

@ -20,7 +20,7 @@ export default {
computed: { computed: {
contents() { contents() {
const obj = { const obj = {
title: 'You haven\'t added any issues to your project yet', title: "You haven't added any issues to your project yet",
content: ` content: `
An issue can be a bug, a todo or a feature request that needs to be An issue can be a bug, a todo or a feature request that needs to be
discussed in a project. Besides, issues are searchable and filterable. discussed in a project. Besides, issues are searchable and filterable.
@ -28,7 +28,7 @@ export default {
}; };
if (this.activeTab === 'selected') { if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet'; obj.title = "You haven't selected any issues yet";
obj.content = ` obj.content = `
Go back to <strong>Open issues</strong> and select some issues Go back to <strong>Open issues</strong> and select some issues
to add to your board. to add to your board.

View file

@ -5,6 +5,7 @@ import ListsDropdown from './lists_dropdown.vue';
import { pluralize } from '../../../lib/utils/text_utility'; import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
import boardsStore from '../../stores/boards_store';
export default { export default {
components: { components: {
@ -14,7 +15,7 @@ export default {
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state, state: boardsStore.state,
}; };
}, },
computed: { computed: {
@ -41,19 +42,17 @@ export default {
const req = this.buildUpdateRequest(list); const req = this.buildUpdateRequest(list);
// Post the data to the backend // Post the data to the backend
gl.boardService gl.boardService.bulkUpdate(issueIds, req).catch(() => {
.bulkUpdate(issueIds, req) Flash(__('Failed to update issues, please try again.'));
.catch(() => {
Flash(__('Failed to update issues, please try again.'));
selectedIssues.forEach((issue) => { selectedIssues.forEach(issue => {
list.removeIssue(issue); list.removeIssue(issue);
list.issuesSize -= 1; list.issuesSize -= 1;
});
}); });
});
// Add the issues on the frontend // Add the issues on the frontend
selectedIssues.forEach((issue) => { selectedIssues.forEach(issue => {
list.addIssue(issue); list.addIssue(issue);
list.issuesSize += 1; list.issuesSize += 1;
}); });

View file

@ -1,52 +1,52 @@
<script> <script>
import ModalFilters from './filters'; import ModalFilters from './filters';
import ModalTabs from './tabs.vue'; import ModalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
export default { export default {
components: { components: {
ModalTabs, ModalTabs,
ModalFilters, ModalFilters,
},
mixins: [modalMixin],
props: {
projectId: {
type: Number,
required: true,
}, },
mixins: [modalMixin], milestonePath: {
props: { type: String,
projectId: { required: true,
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
}, },
data() { labelPath: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { },
selectAllText() { data() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { return ModalStore.store;
return 'Select all'; },
} computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return 'Select all';
}
return 'Deselect all'; return 'Deselect all';
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
}, },
methods: { showSearch() {
toggleAll() { return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
this.$refs.selectAllBtn.blur(); },
},
methods: {
toggleAll() {
this.$refs.selectAllBtn.blur();
ModalStore.toggleAll(); ModalStore.toggleAll();
},
}, },
}; },
};
</script> </script>
<template> <template>
<div> <div>

View file

@ -1,143 +1,146 @@
<script> <script>
/* global ListIssue */ /* global ListIssue */
import { urlParamsToObject } from '~/lib/utils/common_utils'; import { urlParamsToObject } from '~/lib/utils/common_utils';
import ModalHeader from './header.vue'; import ModalHeader from './header.vue';
import ModalList from './list.vue'; import ModalList from './list.vue';
import ModalFooter from './footer.vue'; import ModalFooter from './footer.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default { export default {
components: { components: {
EmptyState, EmptyState,
ModalHeader, ModalHeader,
ModalList, ModalList,
ModalFooter, ModalFooter,
GlLoadingIcon,
},
props: {
newIssuePath: {
type: String,
required: true,
}, },
props: { emptyStateSvg: {
newIssuePath: { type: String,
type: String, required: true,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
}, },
data() { issueLinkBase: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { rootPath: {
showList() { type: String,
if (this.activeTab === 'selected') { required: true,
return this.selectedIssues.length > 0; },
} projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0; return this.issuesCount > 0;
},
showEmptyState() {
if (!this.loading && this.issuesCount === 0) {
return true;
}
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
}, },
watch: { showEmptyState() {
page() { if (!this.loading && this.issuesCount === 0) {
this.loadIssues(); return true;
}, }
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) { return this.activeTab === 'selected' && this.selectedIssues.length === 0;
this.loading = true; },
},
watch: {
page() {
this.loadIssues();
},
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
const loadingDone = () => {
this.loading = false;
};
this.loadIssues()
.then(loadingDone)
.catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
}
},
filter: {
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
const loadingDone = () => { const loadingDone = () => {
this.loading = false; this.filterLoading = false;
}; };
this.loadIssues() this.loadIssues(true)
.then(loadingDone) .then(loadingDone)
.catch(loadingDone); .catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
} }
}, },
filter: { deep: true,
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
const loadingDone = () => {
this.filterLoading = false;
};
this.loadIssues(true)
.then(loadingDone)
.catch(loadingDone);
}
},
deep: true,
},
}, },
created() { },
this.page = 1; created() {
}, this.page = 1;
methods: { },
loadIssues(clearIssues = false) { methods: {
if (!this.showAddIssuesModal) return false; loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
return gl.boardService.getBacklog({ return gl.boardService
.getBacklog({
...urlParamsToObject(this.filter.path), ...urlParamsToObject(this.filter.path),
page: this.page, page: this.page,
per: this.perPage, per: this.perPage,
}) })
.then(res => res.data) .then(res => res.data)
.then(data => { .then(data => {
if (clearIssues) { if (clearIssues) {
this.issues = []; this.issues = [];
} }
data.issues.forEach(issueObj => { data.issues.forEach(issueObj => {
const issue = new ListIssue(issueObj); const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue); const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
issue.selected = !!foundSelectedIssue; issue.selected = !!foundSelectedIssue;
this.issues.push(issue); this.issues.push(issue);
});
this.loadingNewPage = false;
if (!this.issuesCount) {
this.issuesCount = data.size;
}
})
.catch(() => {
// TODO: handle request error
}); });
},
this.loadingNewPage = false;
if (!this.issuesCount) {
this.issuesCount = data.size;
}
})
.catch(() => {
// TODO: handle request error
});
}, },
}; },
};
</script> </script>
<template> <template>
<div <div

View file

@ -1,118 +1,120 @@
<script> <script>
import bp from '../../../breakpoints'; import Icon from '~/vue_shared/components/icon.vue';
import ModalStore from '../../stores/modal_store'; import bp from '../../../breakpoints';
import IssueCardInner from '../issue_card_inner.vue'; import ModalStore from '../../stores/modal_store';
import IssueCardInner from '../issue_card_inner.vue';
export default { export default {
components: { components: {
IssueCardInner, IssueCardInner,
Icon,
},
props: {
issueLinkBase: {
type: String,
required: true,
}, },
props: { rootPath: {
issueLinkBase: { type: String,
type: String, required: true,
required: true,
},
rootPath: {
type: String,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
}, },
data() { emptyStateSvg: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { },
loopIssues() { data() {
if (this.activeTab === 'all') { return ModalStore.store;
return this.issues; },
computed: {
loopIssues() {
if (this.activeTab === 'all') {
return this.issues;
}
return this.selectedIssues;
},
groupedIssues() {
const groups = [];
this.loopIssues.forEach((issue, i) => {
const index = i % this.columns;
if (!groups[index]) {
groups.push([]);
} }
return this.selectedIssues; groups[index].push(issue);
}, });
groupedIssues() {
const groups = [];
this.loopIssues.forEach((issue, i) => {
const index = i % this.columns;
if (!groups[index]) { return groups;
groups.push([]);
}
groups[index].push(issue);
});
return groups;
},
}, },
watch: { },
activeTab() { watch: {
if (this.activeTab === 'all') { activeTab() {
ModalStore.purgeUnselectedIssues(); if (this.activeTab === 'all') {
} ModalStore.purgeUnselectedIssues();
}, }
}, },
mounted() { },
this.scrollHandlerWrapper = this.scrollHandler.bind(this); mounted() {
this.setColumnCountWrapper = this.setColumnCount.bind(this); this.scrollHandlerWrapper = this.scrollHandler.bind(this);
this.setColumnCount(); this.setColumnCountWrapper = this.setColumnCount.bind(this);
this.setColumnCount();
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
window.addEventListener('resize', this.setColumnCountWrapper); window.addEventListener('resize', this.setColumnCountWrapper);
},
beforeDestroy() {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
window.removeEventListener('resize', this.setColumnCountWrapper);
},
methods: {
scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage);
if (
this.scrollTop() > this.scrollHeight() - 100 &&
!this.loadingNewPage &&
currentPage === this.page
) {
this.loadingNewPage = true;
this.page += 1;
}
}, },
beforeDestroy() { toggleIssue(e, issue) {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); if (e.target.tagName !== 'A') {
window.removeEventListener('resize', this.setColumnCountWrapper); ModalStore.toggleIssue(issue);
}
}, },
methods: { listHeight() {
scrollHandler() { return this.$refs.list.getBoundingClientRect().height;
const currentPage = Math.floor(this.issues.length / this.perPage);
if (
this.scrollTop() > this.scrollHeight() - 100 &&
!this.loadingNewPage &&
currentPage === this.page
) {
this.loadingNewPage = true;
this.page += 1;
}
},
toggleIssue(e, issue) {
if (e.target.tagName !== 'A') {
ModalStore.toggleIssue(issue);
}
},
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
showIssue(issue) {
if (this.activeTab === 'all') return true;
const index = ModalStore.selectedIssueIndex(issue);
return index !== -1;
},
setColumnCount() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'lg' || breakpoint === 'md') {
this.columns = 3;
} else if (breakpoint === 'sm') {
this.columns = 2;
} else {
this.columns = 1;
}
},
}, },
}; scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
showIssue(issue) {
if (this.activeTab === 'all') return true;
const index = ModalStore.selectedIssueIndex(issue);
return index !== -1;
},
setColumnCount() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'lg' || breakpoint === 'md') {
this.columns = 3;
} else if (breakpoint === 'sm') {
this.columns = 2;
} else {
this.columns = 1;
}
},
},
};
</script> </script>
<template> <template>
<section <section
@ -147,13 +149,13 @@
:issue="issue" :issue="issue"
:issue-link-base="issueLinkBase" :issue-link-base="issueLinkBase"
:root-path="rootPath"/> :root-path="rootPath"/>
<span <icon
v-if="issue.selected" v-if="issue.selected"
:aria-label="'Issue #' + issue.id + ' selected'" :aria-label="'Issue #' + issue.id + ' selected'"
name="mobile-issue-close"
aria-checked="true" aria-checked="true"
class="issue-card-selected text-center"> class="issue-card-selected text-center"
<i class="fa fa-check"></i> />
</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,15 +1,18 @@
<script> <script>
import { Link } from '@gitlab-org/gitlab-ui'; import { GlLink } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import boardsStore from '../../stores/boards_store';
export default { export default {
components: { components: {
'gl-link': Link, GlLink,
Icon,
}, },
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state, state: boardsStore.state,
}; };
}, },
computed: { computed: {
@ -34,7 +37,9 @@ export default {
class="dropdown-label-box"> class="dropdown-label-box">
</span> </span>
{{ selected.title }} {{ selected.title }}
<i class="fa fa-chevron-down"></i> <icon
name="chevron-down"
/>
</button> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul> <ul>

View file

@ -1,21 +1,21 @@
<script> <script>
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
export default { export default {
mixins: [modalMixin], mixins: [modalMixin],
data() { data() {
return ModalStore.store; return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
}, },
computed: { },
selectedCount() { destroyed() {
return ModalStore.selectedCount(); this.activeTab = 'all';
}, },
}, };
destroyed() {
this.activeTab = 'all';
},
};
</script> </script>
<template> <template>
<div class="top-area prepend-top-10 append-bottom-10"> <div class="top-area prepend-top-10 append-bottom-10">

View file

@ -4,42 +4,43 @@ import $ from 'jquery';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import _ from 'underscore'; import _ from 'underscore';
import CreateLabelDropdown from '../../create_label'; import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store';
window.gl = window.gl || {}; $(document)
window.gl.issueBoards = window.gl.issueBoards || {}; .off('created.label')
.on('created.label', (e, label) => {
const Store = gl.issueBoards.BoardsStore; boardsStore.new({
$(document).off('created.label').on('created.label', (e, label) => {
Store.new({
title: label.title,
position: Store.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title, title: label.title,
color: label.color, position: boardsStore.state.lists.length - 2,
}, list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color,
},
});
}); });
});
gl.issueBoards.newListDropdownInit = () => { export default function initNewListDropdown() {
$('.js-new-board-list').each(function () { $('.js-new-board-list').each(function() {
const $this = $(this); const $this = $(this);
new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath')); new CreateLabelDropdown(
$this.closest('.dropdown').find('.dropdown-new-label'),
$this.data('namespacePath'),
$this.data('projectPath'),
);
$this.glDropdown({ $this.glDropdown({
data(term, callback) { data(term, callback) {
axios.get($this.attr('data-list-labels-path')) axios.get($this.attr('data-list-labels-path')).then(({ data }) => {
.then(({ data }) => { callback(data);
callback(data); });
});
}, },
renderRow (label) { renderRow(label) {
const active = Store.findList('title', label.title); const active = boardsStore.findList('title', label.title);
const $li = $('<li />'); const $li = $('<li />');
const $a = $('<a />', { const $a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''), class: active ? `is-active js-board-list-${active.id}` : '',
text: label.title, text: label.title,
href: '#', href: '#',
}); });
@ -57,15 +58,15 @@ gl.issueBoards.newListDropdownInit = () => {
selectable: true, selectable: true,
multiSelect: true, multiSelect: true,
containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content', containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
clicked (options) { clicked(options) {
const { e } = options; const { e } = options;
const label = options.selectedObj; const label = options.selectedObj;
e.preventDefault(); e.preventDefault();
if (!Store.findList('title', label.title)) { if (!boardsStore.findList('title', label.title)) {
Store.new({ boardsStore.new({
title: label.title, title: label.title,
position: Store.state.lists.length - 2, position: boardsStore.state.lists.length - 2,
list_type: 'label', list_type: 'label',
label: { label: {
id: label.id, id: label.id,
@ -74,9 +75,9 @@ gl.issueBoards.newListDropdownInit = () => {
}, },
}); });
Store.state.lists = _.sortBy(Store.state.lists, 'position'); boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position');
} }
}, },
}); });
}); });
}; }

View file

@ -1,11 +1,17 @@
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import Api from '../../api'; import Api from '../../api';
export default { export default {
name: 'BoardProjectSelect', name: 'BoardProjectSelect',
components: {
Icon,
GlLoadingIcon,
},
props: { props: {
groupId: { groupId: {
type: Number, type: Number,
@ -42,7 +48,7 @@ export default {
selectable: true, selectable: true,
data: (term, callback) => { data: (term, callback) => {
this.loading = true; this.loading = true;
return Api.groupProjects(this.groupId, term, {}, projects => { return Api.groupProjects(this.groupId, term, { with_issues_enabled: true }, projects => {
this.loading = false; this.loading = false;
callback(projects); callback(projects);
}); });
@ -50,7 +56,9 @@ export default {
renderRow(project) { renderRow(project) {
return ` return `
<li> <li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}"> <a href='#' class='dropdown-menu-link' data-project-id="${
project.id
}" data-project-name="${project.name}">
${_.escape(project.name)} ${_.escape(project.name)}
</a> </a>
</li> </li>
@ -78,11 +86,9 @@ export default {
aria-expanded="false" aria-expanded="false"
> >
{{ selectedProjectName }} {{ selectedProjectName }}
<i <icon
class="fa fa-chevron-down" name="chevron-down"
aria-hidden="true" />
>
</i>
</button> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width"> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title"> <div class="dropdown-title">
@ -92,12 +98,11 @@ export default {
type="button" type="button"
class="dropdown-title-button dropdown-menu-close" class="dropdown-title-button dropdown-menu-close"
> >
<i <icon
aria-hidden="true" name="merge-request-close-m"
data-hidden="true" data-hidden="true"
class="fa fa-times dropdown-menu-close-icon" class="dropdown-menu-close-icon"
> />
</i>
</button> </button>
</div> </div>
<div class="dropdown-input"> <div class="dropdown-input">
@ -106,12 +111,11 @@ export default {
type="search" type="search"
placeholder="Search projects" placeholder="Search projects"
/> />
<i <icon
aria-hidden="true" name="search"
class="dropdown-input-search"
data-hidden="true" data-hidden="true"
class="fa fa-search dropdown-input-search" />
>
</i>
</div> </div>
<div class="dropdown-content"></div> <div class="dropdown-content"></div>
<div class="dropdown-loading"> <div class="dropdown-loading">

View file

@ -1,80 +1,77 @@
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import boardsStore from '../../stores/boards_store';
const Store = gl.issueBoards.BoardsStore; export default Vue.extend({
props: {
export default Vue.extend({ issue: {
props: { type: Object,
issue: { required: true,
type: Object,
required: true,
},
list: {
type: Object,
required: true,
},
}, },
computed: { list: {
updateUrl() { type: Object,
return this.issue.path; required: true,
},
}, },
methods: { },
removeIssue() { computed: {
const { issue } = this; updateUrl() {
const lists = issue.getLists(); return this.issue.path;
const req = this.buildPatchRequest(issue, lists); },
},
methods: {
removeIssue() {
const { issue } = this;
const lists = issue.getLists();
const req = this.buildPatchRequest(issue, lists);
const data = { const data = {
issue: this.seedPatchRequest(issue, req), issue: this.seedPatchRequest(issue, req),
}; };
if (data.issue.label_ids.length === 0) { if (data.issue.label_ids.length === 0) {
data.issue.label_ids = ['']; data.issue.label_ids = [''];
} }
// Post the remove data // Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => { Vue.http.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.')); Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach(list => {
list.addIssue(issue);
});
});
// Remove from the frontend store
lists.forEach(list => { lists.forEach(list => {
list.removeIssue(issue); list.addIssue(issue);
}); });
});
Store.detail.issue = {}; // Remove from the frontend store
}, lists.forEach(list => {
/** list.removeIssue(issue);
* Build the default patch request. });
*/
buildPatchRequest(issue, lists) {
const listLabelIds = lists.map(list => list.label.id);
const labelIds = issue.labels boardsStore.detail.issue = {};
.map(label => label.id)
.filter(id => !listLabelIds.includes(id));
return {
label_ids: labelIds,
};
},
/**
* Seed the given patch request.
*
* (This is overridden in EE)
*/
seedPatchRequest(issue, req) {
return req;
},
}, },
}); /**
* Build the default patch request.
*/
buildPatchRequest(issue, lists) {
const listLabelIds = lists.map(list => list.label.id);
const labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
return {
label_ids: labelIds,
};
},
/**
* Seed the given patch request.
*
* (This is overridden in EE)
*/
seedPatchRequest(issue, req) {
return req;
},
},
});
</script> </script>
<template> <template>
<div <div

View file

@ -1,5 +1,6 @@
import FilteredSearchContainer from '../filtered_search/container'; import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager'; import FilteredSearchManager from '../filtered_search/filtered_search_manager';
import boardsStore from './stores/boards_store';
export default class FilteredSearchBoards extends FilteredSearchManager { export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) { constructor(store, updateUrl = false, cantEdit = []) {
@ -23,7 +24,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
this.store.path = path.substr(1); this.store.path = path.substr(1);
if (this.updateUrl) { if (this.updateUrl) {
gl.issueBoards.BoardsStore.updateFiltersUrl(); boardsStore.updateFiltersUrl();
} }
} }
@ -31,7 +32,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token'); const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
// Remove all the tokens as they will be replaced by the search manager // Remove all the tokens as they will be replaced by the search manager
[].forEach.call(tokens, (el) => { [].forEach.call(tokens, el => {
el.parentNode.removeChild(el); el.parentNode.removeChild(el);
}); });
@ -49,7 +50,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
canEdit(tokenName, tokenValue) { canEdit(tokenName, tokenValue) {
if (this.cantEdit.includes(tokenName)) return false; if (this.cantEdit.includes(tokenName)) return false;
return this.cantEditWithValue.findIndex(token => token.name === tokenName && return (
token.value === tokenValue) === -1; this.cantEditWithValue.findIndex(
token => token.name === tokenName && token.value === tokenValue,
) === -1
);
} }
} }

View file

@ -14,54 +14,48 @@ import './models/issue';
import './models/list'; import './models/list';
import './models/milestone'; import './models/milestone';
import './models/project'; import './models/project';
import './stores/boards_store'; import boardsStore from './stores/boards_store';
import ModalStore from './stores/modal_store'; import ModalStore from './stores/modal_store';
import BoardService from './services/board_service'; import BoardService from './services/board_service';
import modalMixin from './mixins/modal_mixins'; import modalMixin from './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters'; import './filters/due_date_filters';
import './components/board'; import Board from './components/board';
import './components/board_sidebar'; import BoardSidebar from './components/board_sidebar';
import './components/new_list_dropdown'; import initNewListDropdown from './components/new_list_dropdown';
import BoardAddIssuesModal from './components/modal/index.vue'; import BoardAddIssuesModal from './components/modal/index.vue';
import '~/vue_shared/vue_resource_interceptor'; import '~/vue_shared/vue_resource_interceptor';
import { NavigationType } from '~/lib/utils/common_utils'; import { NavigationType } from '~/lib/utils/common_utils';
let issueBoardsApp;
export default () => { export default () => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
// check for browser back and trigger a hard reload to circumvent browser caching. // check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => { window.addEventListener('pageshow', event => {
const isNavTypeBackForward = window.performance && const isNavTypeBackForward =
window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD; window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
if (event.persisted || isNavTypeBackForward) { if (event.persisted || isNavTypeBackForward) {
window.location.reload(); window.location.reload();
} }
}); });
if (gl.IssueBoardsApp) { if (issueBoardsApp) {
gl.IssueBoardsApp.$destroy(true); issueBoardsApp.$destroy(true);
} }
Store.create(); boardsStore.create();
// hack to allow sidebar scripts like milestone_select manipulate the BoardsStore issueBoardsApp = new Vue({
gl.issueBoards.boardStoreIssueSet = (...args) => Vue.set(Store.detail.issue, ...args);
gl.issueBoards.boardStoreIssueDelete = (...args) => Vue.delete(Store.detail.issue, ...args);
gl.IssueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
components: { components: {
board: gl.issueBoards.Board, Board,
'board-sidebar': gl.issueBoards.BoardSidebar, BoardSidebar,
BoardAddIssuesModal, BoardAddIssuesModal,
}, },
data: { data: {
state: Store.state, state: boardsStore.state,
loading: true, loading: true,
boardsEndpoint: $boardApp.dataset.boardsEndpoint, boardsEndpoint: $boardApp.dataset.boardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint, listsEndpoint: $boardApp.dataset.listsEndpoint,
@ -70,7 +64,7 @@ export default () => {
issueLinkBase: $boardApp.dataset.issueLinkBase, issueLinkBase: $boardApp.dataset.issueLinkBase,
rootPath: $boardApp.dataset.rootPath, rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: Store.detail, detailIssue: boardsStore.detail,
defaultAvatar: $boardApp.dataset.defaultAvatar, defaultAvatar: $boardApp.dataset.defaultAvatar,
}, },
computed: { computed: {
@ -85,7 +79,7 @@ export default () => {
bulkUpdatePath: this.bulkUpdatePath, bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId, boardId: this.boardId,
}); });
Store.rootPath = this.boardsEndpoint; boardsStore.rootPath = this.boardsEndpoint;
eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('updateTokens', this.updateTokens);
eventHub.$on('newDetailIssue', this.updateDetailIssue); eventHub.$on('newDetailIssue', this.updateDetailIssue);
@ -99,16 +93,16 @@ export default () => {
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
}, },
mounted() { mounted() {
this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit); this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit);
this.filterManager.setup(); this.filterManager.setup();
Store.disabled = this.disabled; boardsStore.disabled = this.disabled;
gl.boardService gl.boardService
.all() .all()
.then(res => res.data) .then(res => res.data)
.then(data => { .then(data => {
data.forEach(board => { data.forEach(board => {
const list = Store.addList(board, this.defaultAvatar); const list = boardsStore.addList(board, this.defaultAvatar);
if (list.type === 'closed') { if (list.type === 'closed') {
list.position = Infinity; list.position = Infinity;
@ -119,7 +113,7 @@ export default () => {
this.state.lists = _.sortBy(this.state.lists, 'position'); this.state.lists = _.sortBy(this.state.lists, 'position');
Store.addBlankState(); boardsStore.addBlankState();
this.loading = false; this.loading = false;
}) })
.catch(() => { .catch(() => {
@ -148,13 +142,13 @@ export default () => {
}); });
} }
Store.detail.issue = newIssue; boardsStore.detail.issue = newIssue;
}, },
clearDetailIssue() { clearDetailIssue() {
Store.detail.issue = {}; boardsStore.detail.issue = {};
}, },
toggleSubscription(id) { toggleSubscription(id) {
const { issue } = Store.detail; const { issue } = boardsStore.detail;
if (issue.id === id && issue.toggleSubscriptionEndpoint) { if (issue.id === id && issue.toggleSubscriptionEndpoint) {
issue.setFetchingState('subscriptions', true); issue.setFetchingState('subscriptions', true);
BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint) BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
@ -173,26 +167,28 @@ export default () => {
}, },
}); });
gl.IssueBoardsSearch = new Vue({ // eslint-disable-next-line no-new
new Vue({
el: document.getElementById('js-add-list'), el: document.getElementById('js-add-list'),
data: { data: {
filters: Store.state.filters, filters: boardsStore.state.filters,
}, },
mounted() { mounted() {
gl.issueBoards.newListDropdownInit(); initNewListDropdown();
}, },
}); });
const issueBoardsModal = document.getElementById('js-add-issues-btn'); const issueBoardsModal = document.getElementById('js-add-issues-btn');
if (issueBoardsModal) { if (issueBoardsModal) {
gl.IssueBoardsModalAddBtn = new Vue({ // eslint-disable-next-line no-new
new Vue({
el: issueBoardsModal, el: issueBoardsModal,
mixins: [modalMixin], mixins: [modalMixin],
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
store: Store.state, store: boardsStore.state,
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
}; };
}, },

View file

@ -3,32 +3,33 @@
import $ from 'jquery'; import $ from 'jquery';
import sortableConfig from '../../sortable/sortable_config'; import sortableConfig from '../../sortable/sortable_config';
window.gl = window.gl || {}; export function sortableStart() {
window.gl.issueBoards = window.gl.issueBoards || {}; $('.has-tooltip')
.tooltip('hide')
gl.issueBoards.onStart = () => {
$('.has-tooltip').tooltip('hide')
.tooltip('disable'); .tooltip('disable');
document.body.classList.add('is-dragging'); document.body.classList.add('is-dragging');
}; }
gl.issueBoards.onEnd = () => { export function sortableEnd() {
$('.has-tooltip').tooltip('enable'); $('.has-tooltip').tooltip('enable');
document.body.classList.remove('is-dragging'); document.body.classList.remove('is-dragging');
}; }
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; export function getBoardSortableDefaultOptions(obj) {
const touchEnabled =
'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
const defaultSortOptions = Object.assign({}, sortableConfig, { const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn', filter: '.board-delete, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0, delay: touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSensitivity: touchEnabled ? 60 : 100,
scrollSpeed: 20, scrollSpeed: 20,
onStart: gl.issueBoards.onStart, onStart: sortableStart,
onEnd: gl.issueBoards.onEnd, onEnd: sortableEnd,
}); });
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); Object.keys(obj).forEach(key => {
defaultSortOptions[key] = obj[key];
});
return defaultSortOptions; return defaultSortOptions;
}; }

View file

@ -1,4 +1,4 @@
/* eslint-disable no-unused-vars, comma-dangle */ /* eslint-disable no-unused-vars */
/* global ListLabel */ /* global ListLabel */
/* global ListMilestone */ /* global ListMilestone */
/* global ListAssignee */ /* global ListAssignee */
@ -6,9 +6,10 @@
import Vue from 'vue'; import Vue from 'vue';
import '~/vue_shared/models/label'; import '~/vue_shared/models/label';
import IssueProject from './project'; import IssueProject from './project';
import boardsStore from '../stores/boards_store';
class ListIssue { class ListIssue {
constructor (obj, defaultAvatar) { constructor(obj, defaultAvatar) {
this.id = obj.id; this.id = obj.id;
this.iid = obj.iid; this.iid = obj.iid;
this.title = obj.title; this.title = obj.title;
@ -29,6 +30,8 @@ class ListIssue {
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id; this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id; this.project_id = obj.project_id;
this.timeEstimate = obj.time_estimate;
this.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
if (obj.project) { if (obj.project) {
this.project = new IssueProject(obj.project); this.project = new IssueProject(obj.project);
@ -38,55 +41,55 @@ class ListIssue {
this.milestone = new ListMilestone(obj.milestone); this.milestone = new ListMilestone(obj.milestone);
} }
obj.labels.forEach((label) => { obj.labels.forEach(label => {
this.labels.push(new ListLabel(label)); this.labels.push(new ListLabel(label));
}); });
this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar)); this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
} }
addLabel (label) { addLabel(label) {
if (!this.findLabel(label)) { if (!this.findLabel(label)) {
this.labels.push(new ListLabel(label)); this.labels.push(new ListLabel(label));
} }
} }
findLabel (findLabel) { findLabel(findLabel) {
return this.labels.filter(label => label.title === findLabel.title)[0]; return this.labels.filter(label => label.title === findLabel.title)[0];
} }
removeLabel (removeLabel) { removeLabel(removeLabel) {
if (removeLabel) { if (removeLabel) {
this.labels = this.labels.filter(label => removeLabel.title !== label.title); this.labels = this.labels.filter(label => removeLabel.title !== label.title);
} }
} }
removeLabels (labels) { removeLabels(labels) {
labels.forEach(this.removeLabel.bind(this)); labels.forEach(this.removeLabel.bind(this));
} }
addAssignee (assignee) { addAssignee(assignee) {
if (!this.findAssignee(assignee)) { if (!this.findAssignee(assignee)) {
this.assignees.push(new ListAssignee(assignee)); this.assignees.push(new ListAssignee(assignee));
} }
} }
findAssignee (findAssignee) { findAssignee(findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0]; return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
} }
removeAssignee (removeAssignee) { removeAssignee(removeAssignee) {
if (removeAssignee) { if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id); this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
} }
} }
removeAllAssignees () { removeAllAssignees() {
this.assignees = []; this.assignees = [];
} }
getLists () { getLists() {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); return boardsStore.state.lists.filter(list => list.findIssue(this.id));
} }
updateData(newData) { updateData(newData) {
@ -101,14 +104,14 @@ class ListIssue {
this.isLoading[key] = value; this.isLoading[key] = value;
} }
update () { update() {
const data = { const data = {
issue: { issue: {
milestone_id: this.milestone ? this.milestone.id : null, milestone_id: this.milestone ? this.milestone.id : null,
due_date: this.dueDate, due_date: this.dueDate,
assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0], assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0],
label_ids: this.labels.map((label) => label.id) label_ids: this.labels.map(label => label.id),
} },
}; };
if (!data.issue.label_ids.length) { if (!data.issue.label_ids.length) {

View file

@ -1,10 +1,11 @@
/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len */ /* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign */
/* global ListIssue */ /* global ListIssue */
import { __ } from '~/locale'; import { __ } from '~/locale';
import ListLabel from '~/vue_shared/models/label'; import ListLabel from '~/vue_shared/models/label';
import ListAssignee from '~/vue_shared/models/assignee'; import ListAssignee from '~/vue_shared/models/assignee';
import { urlParamsToObject } from '~/lib/utils/common_utils'; import { urlParamsToObject } from '~/lib/utils/common_utils';
import boardsStore from '../stores/boards_store';
const PER_PAGE = 20; const PER_PAGE = 20;
@ -89,9 +90,9 @@ class List {
} }
destroy() { destroy() {
const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this); const index = boardsStore.state.lists.indexOf(this);
gl.issueBoards.BoardsStore.state.lists.splice(index, 1); boardsStore.state.lists.splice(index, 1);
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); boardsStore.updateNewListDropdown(this.id);
gl.boardService.destroyList(this.id).catch(() => { gl.boardService.destroyList(this.id).catch(() => {
// TODO: handle request error // TODO: handle request error
@ -116,7 +117,7 @@ class List {
getIssues(emptyIssues = true) { getIssues(emptyIssues = true) {
const data = { const data = {
...urlParamsToObject(gl.issueBoards.BoardsStore.filter.path), ...urlParamsToObject(boardsStore.filter.path),
page: this.page, page: this.page,
}; };
@ -233,11 +234,11 @@ class List {
}); });
} }
getTypeInfo (type) { getTypeInfo(type) {
return TYPES[type] || {}; return TYPES[type] || {};
} }
onNewIssueResponse (issue, data) { onNewIssueResponse(issue, data) {
issue.id = data.id; issue.id = data.id;
issue.iid = data.iid; issue.iid = data.iid;
issue.project = data.project; issue.project = data.project;

View file

@ -19,7 +19,9 @@ export default class BoardService {
} }
static generateIssuePath(boardId, id) { static generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`; return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
id ? `/${id}` : ''
}`;
} }
all() { all() {
@ -54,7 +56,9 @@ export default class BoardService {
getIssuesForList(id, filter = {}) { getIssuesForList(id, filter = {}) {
const data = { id }; const data = { id };
Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); Object.keys(filter).forEach(key => {
data[key] = filter[key];
});
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
} }
@ -75,7 +79,9 @@ export default class BoardService {
} }
getBacklog(data) { getBacklog(data) {
return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`)); return axios.get(
mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`),
);
} }
bulkUpdate(issueIds, extraData = {}) { bulkUpdate(issueIds, extraData = {}) {

View file

@ -1,15 +1,13 @@
/* eslint-disable comma-dangle, no-shadow */ /* eslint-disable no-shadow */
/* global List */ /* global List */
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { getUrlParamsArray } from '~/lib/utils/common_utils'; import { getUrlParamsArray } from '~/lib/utils/common_utils';
window.gl = window.gl || {}; const boardsStore = {
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardsStore = {
disabled: false, disabled: false,
filter: { filter: {
path: '', path: '',
@ -22,20 +20,20 @@ gl.issueBoards.BoardsStore = {
issue: {}, issue: {},
list: {}, list: {},
}, },
create () { create() {
this.state.lists = []; this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&'); this.filter.path = getUrlParamsArray().join('&');
this.detail = { this.detail = {
issue: {}, issue: {},
}; };
}, },
addList (listObj, defaultAvatar) { addList(listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar); const list = new List(listObj, defaultAvatar);
this.state.lists.push(list); this.state.lists.push(list);
return list; return list;
}, },
new (listObj) { new(listObj) {
const list = this.addList(listObj); const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog'); const backlogList = this.findList('type', 'backlog', 'backlog');
@ -52,44 +50,44 @@ gl.issueBoards.BoardsStore = {
}); });
this.removeBlankState(); this.removeBlankState();
}, },
updateNewListDropdown (listId) { updateNewListDropdown(listId) {
$(`.js-board-list-${listId}`).removeClass('is-active'); $(`.js-board-list-${listId}`).removeClass('is-active');
}, },
shouldAddBlankState () { shouldAddBlankState() {
// Decide whether to add the blank state // Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]); return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0];
}, },
addBlankState () { addBlankState() {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
this.addList({ this.addList({
id: 'blank', id: 'blank',
list_type: 'blank', list_type: 'blank',
title: 'Welcome to your Issue Board!', title: 'Welcome to your Issue Board!',
position: 0 position: 0,
}); });
this.state.lists = _.sortBy(this.state.lists, 'position'); this.state.lists = _.sortBy(this.state.lists, 'position');
}, },
removeBlankState () { removeBlankState() {
this.removeList('blank'); this.removeList('blank');
Cookies.set('issue_board_welcome_hidden', 'true', { Cookies.set('issue_board_welcome_hidden', 'true', {
expires: 365 * 10, expires: 365 * 10,
path: '' path: '',
}); });
}, },
welcomeIsHidden () { welcomeIsHidden() {
return Cookies.get('issue_board_welcome_hidden') === 'true'; return Cookies.get('issue_board_welcome_hidden') === 'true';
}, },
removeList (id, type = 'blank') { removeList(id, type = 'blank') {
const list = this.findList('id', id, type); const list = this.findList('id', id, type);
if (!list) return; if (!list) return;
this.state.lists = this.state.lists.filter(list => list.id !== id); this.state.lists = this.state.lists.filter(list => list.id !== id);
}, },
moveList (listFrom, orderLists) { moveList(listFrom, orderLists) {
orderLists.forEach((id, i) => { orderLists.forEach((id, i) => {
const list = this.findList('id', parseInt(id, 10)); const list = this.findList('id', parseInt(id, 10));
@ -97,22 +95,25 @@ gl.issueBoards.BoardsStore = {
}); });
listFrom.update(); listFrom.update();
}, },
moveIssueToList (listFrom, listTo, issue, newIndex) { moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id); const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists(); const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label); const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) { if (!issueTo) {
// Check if target list assignee is already present in this issue // Check if target list assignee is already present in this issue
if ((listTo.type === 'assignee' && listFrom.type === 'assignee') && if (
issue.findAssignee(listTo.assignee)) { listTo.type === 'assignee' &&
listFrom.type === 'assignee' &&
issue.findAssignee(listTo.assignee)
) {
const targetIssue = listTo.findIssue(issue.id); const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee); targetIssue.removeAssignee(listFrom.assignee);
} else if (listTo.type === 'milestone') { } else if (listTo.type === 'milestone') {
const currentMilestone = issue.milestone; const currentMilestone = issue.milestone;
const currentLists = this.state.lists const currentLists = this.state.lists
.filter(list => (list.type === 'milestone' && list.id !== listTo.id)) .filter(list => list.type === 'milestone' && list.id !== listTo.id)
.filter(list => list.issues.some(listIssue => issue.id === listIssue.id)); .filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
issue.removeMilestone(currentMilestone); issue.removeMilestone(currentMilestone);
issue.addMilestone(listTo.milestone); issue.addMilestone(listTo.milestone);
@ -128,7 +129,7 @@ gl.issueBoards.BoardsStore = {
} }
if (listTo.type === 'closed' && listFrom.type !== 'backlog') { if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => { issueLists.forEach(list => {
list.removeIssue(issue); list.removeIssue(issue);
}); });
issue.removeLabels(listLabels); issue.removeLabels(listLabels);
@ -146,24 +147,39 @@ gl.issueBoards.BoardsStore = {
return ( return (
(listTo.type !== 'label' && listFrom.type === 'assignee') || (listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') || (listTo.type !== 'assignee' && listFrom.type === 'label') ||
(listFrom.type === 'backlog') listFrom.type === 'backlog'
); );
}, },
moveIssueInList (list, issue, oldIndex, newIndex, idArray) { moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null; const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null; const afterId = parseInt(idArray[newIndex + 1], 10) || null;
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
}, },
findList (key, val, type = 'label') { findList(key, val, type = 'label') {
const filteredList = this.state.lists.filter((list) => { const filteredList = this.state.lists.filter(list => {
const byType = type ? (list.type === type) || (list.type === 'assignee') || (list.type === 'milestone') : true; const byType = type
? list.type === type || list.type === 'assignee' || list.type === 'milestone'
: true;
return list[key] === val && byType; return list[key] === val && byType;
}); });
return filteredList[0]; return filteredList[0];
}, },
updateFiltersUrl () { updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`); window.history.pushState(null, null, `?${this.filter.path}`);
} },
}; };
// hacks added in order to allow milestone_select to function properly
// TODO: remove these
export function boardStoreIssueSet(...args) {
Vue.set(boardsStore.detail.issue, ...args);
}
export function boardStoreIssueDelete(...args) {
Vue.delete(boardsStore.detail.issue, ...args);
}
export default boardsStore;

View file

@ -40,7 +40,7 @@ class ModalStore {
toggleAll() { toggleAll() {
const select = this.selectedCount() !== this.store.issues.length; const select = this.selectedCount() !== this.store.issues.length;
this.store.issues.forEach((issue) => { this.store.issues.forEach(issue => {
const issueUpdate = issue; const issueUpdate = issue;
if (issueUpdate.selected !== select) { if (issueUpdate.selected !== select) {
@ -69,13 +69,14 @@ class ModalStore {
removeSelectedIssue(issue, forcePurge = false) { removeSelectedIssue(issue, forcePurge = false) {
if (this.store.activeTab === 'all' || forcePurge) { if (this.store.activeTab === 'all' || forcePurge) {
this.store.selectedIssues = this.store.selectedIssues this.store.selectedIssues = this.store.selectedIssues.filter(
.filter(fIssue => fIssue.id !== issue.id); fIssue => fIssue.id !== issue.id,
);
} }
} }
purgeUnselectedIssues() { purgeUnselectedIssues() {
this.store.selectedIssues.forEach((issue) => { this.store.selectedIssues.forEach(issue => {
if (!issue.selected) { if (!issue.selected) {
this.removeSelectedIssue(issue, true); this.removeSelectedIssue(issue, true);
} }
@ -87,8 +88,7 @@ class ModalStore {
} }
findSelectedIssue(issue) { findSelectedIssue(issue) {
return this.store.selectedIssues return this.store.selectedIssues.filter(filteredIssue => filteredIssue.id === issue.id)[0];
.filter(filteredIssue => filteredIssue.id === issue.id)[0];
} }
} }

View file

@ -1,6 +1,6 @@
import $ from 'jquery'; import $ from 'jquery';
export const addTooltipToEl = (el) => { export const addTooltipToEl = el => {
const textEl = el.querySelector('.js-breadcrumb-item-text'); const textEl = el.querySelector('.js-breadcrumb-item-text');
if (textEl && textEl.scrollWidth > textEl.offsetWidth) { if (textEl && textEl.scrollWidth > textEl.offsetWidth) {
@ -14,17 +14,18 @@ export default () => {
const breadcrumbs = document.querySelector('.js-breadcrumbs-list'); const breadcrumbs = document.querySelector('.js-breadcrumbs-list');
if (breadcrumbs) { if (breadcrumbs) {
const topLevelLinks = [...breadcrumbs.children].filter(el => !el.classList.contains('dropdown')) const topLevelLinks = [...breadcrumbs.children]
.filter(el => !el.classList.contains('dropdown'))
.map(el => el.querySelector('a')) .map(el => el.querySelector('a'))
.filter(el => el); .filter(el => el);
const $expander = $('.js-breadcrumbs-collapsed-expander'); const $expander = $('.js-breadcrumbs-collapsed-expander');
topLevelLinks.forEach(el => addTooltipToEl(el)); topLevelLinks.forEach(el => addTooltipToEl(el));
$expander.closest('.dropdown') $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', e => {
.on('show.bs.dropdown hide.bs.dropdown', (e) => { $('.js-breadcrumbs-collapsed-expander', e.currentTarget)
$('.js-breadcrumbs-collapsed-expander', e.currentTarget).toggleClass('open') .toggleClass('open')
.tooltip('hide'); .tooltip('hide');
}); });
} }
}; };

View file

@ -12,16 +12,16 @@ export default class BuildArtifacts {
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
disablePropagation() { disablePropagation() {
$('.top-block').on('click', '.download', function (e) { $('.top-block').on('click', '.download', function(e) {
return e.stopPropagation(); return e.stopPropagation();
}); });
return $('.tree-holder').on('click', 'tr[data-link] a', function (e) { return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
return e.stopImmediatePropagation(); return e.stopImmediatePropagation();
}); });
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
setupEntryClick() { setupEntryClick() {
return $('.tree-holder').on('click', 'tr[data-link]', function () { return $('.tree-holder').on('click', 'tr[data-link]', function() {
visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink)); visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
}); });
} }
@ -37,11 +37,15 @@ export default class BuildArtifacts {
// We want the tooltip to show if you hover anywhere on the row // We want the tooltip to show if you hover anywhere on the row
// But be placed below and in the middle of the file name // But be placed below and in the middle of the file name
$('.js-artifact-tree-row') $('.js-artifact-tree-row')
.on('mouseenter', (e) => { .on('mouseenter', e => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show'); $(e.currentTarget)
.find('.js-artifact-tree-tooltip')
.tooltip('show');
}) })
.on('mouseleave', (e) => { .on('mouseleave', e => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide'); $(e.currentTarget)
.find('.js-artifact-tree-tooltip')
.tooltip('hide');
}); });
} }
} }

View file

@ -7,11 +7,13 @@ import statusCodes from '../lib/utils/http_status';
import VariableList from './ci_variable_list'; import VariableList from './ci_variable_list';
function generateErrorBoxContent(errors) { function generateErrorBoxContent(errors) {
const errorList = [].concat(errors).map(errorString => ` const errorList = [].concat(errors).map(
errorString => `
<li> <li>
${_.escape(errorString)} ${_.escape(errorString)}
</li> </li>
`); `,
);
return ` return `
<p> <p>
@ -25,13 +27,7 @@ function generateErrorBoxContent(errors) {
// Used for the variable list on CI/CD projects/groups settings page // Used for the variable list on CI/CD projects/groups settings page
export default class AjaxVariableList { export default class AjaxVariableList {
constructor({ constructor({ container, saveButton, errorBox, formField = 'variables', saveEndpoint }) {
container,
saveButton,
errorBox,
formField = 'variables',
saveEndpoint,
}) {
this.container = container; this.container = container;
this.saveButton = saveButton; this.saveButton = saveButton;
this.errorBox = errorBox; this.errorBox = errorBox;
@ -51,25 +47,28 @@ export default class AjaxVariableList {
} }
onSaveClicked() { onSaveClicked() {
const loadingIcon = this.saveButton.querySelector('.js-secret-variables-save-loading-icon'); const loadingIcon = this.saveButton.querySelector('.js-ci-variables-save-loading-icon');
loadingIcon.classList.toggle('hide', false); loadingIcon.classList.toggle('hide', false);
this.errorBox.classList.toggle('hide', true); this.errorBox.classList.toggle('hide', true);
// We use this to prevent a user from changing a key before we have a chance // We use this to prevent a user from changing a key before we have a chance
// to match it up in `updateRowsWithPersistedVariables` // to match it up in `updateRowsWithPersistedVariables`
this.variableList.toggleEnableRow(false); this.variableList.toggleEnableRow(false);
return axios.patch(this.saveEndpoint, { return axios
variables_attributes: this.variableList.getAllData(), .patch(
}, { this.saveEndpoint,
// We want to be able to process the `res.data` from a 400 error response {
// and print the validation messages such as duplicate variable keys variables_attributes: this.variableList.getAllData(),
validateStatus: status => ( },
status >= statusCodes.OK && {
status < statusCodes.MULTIPLE_CHOICES // We want to be able to process the `res.data` from a 400 error response
) || // and print the validation messages such as duplicate variable keys
status === statusCodes.BAD_REQUEST, validateStatus: status =>
}) (status >= statusCodes.OK && status < statusCodes.MULTIPLE_CHOICES) ||
.then((res) => { status === statusCodes.BAD_REQUEST,
},
)
.then(res => {
loadingIcon.classList.toggle('hide', true); loadingIcon.classList.toggle('hide', true);
this.variableList.toggleEnableRow(true); this.variableList.toggleEnableRow(true);
@ -90,18 +89,21 @@ export default class AjaxVariableList {
} }
updateRowsWithPersistedVariables(persistedVariables = []) { updateRowsWithPersistedVariables(persistedVariables = []) {
const persistedVariableMap = [].concat(persistedVariables).reduce((variableMap, variable) => ({ const persistedVariableMap = [].concat(persistedVariables).reduce(
...variableMap, (variableMap, variable) => ({
[variable.key]: variable, ...variableMap,
}), {}); [variable.key]: variable,
}),
{},
);
this.container.querySelectorAll('.js-row').forEach((row) => { this.container.querySelectorAll('.js-row').forEach(row => {
// If we submitted a row that was destroyed, remove it so we don't try // If we submitted a row that was destroyed, remove it so we don't try
// to destroy it again which would cause a BE error // to destroy it again which would cause a BE error
const destroyInput = row.querySelector('.js-ci-variable-input-destroy'); const destroyInput = row.querySelector('.js-ci-variable-input-destroy');
if (convertPermissionToBoolean(destroyInput.value)) { if (convertPermissionToBoolean(destroyInput.value)) {
row.remove(); row.remove();
// Update the ID input so any future edits and `_destroy` will apply on the BE // Update the ID input so any future edits and `_destroy` will apply on the BE
} else { } else {
const key = row.querySelector('.js-ci-variable-input-key').value; const key = row.querySelector('.js-ci-variable-input-key').value;
const persistedVariable = persistedVariableMap[key]; const persistedVariable = persistedVariableMap[key];

View file

@ -16,10 +16,7 @@ function createEnvironmentItem(value) {
} }
export default class VariableList { export default class VariableList {
constructor({ constructor({ container, formField }) {
container,
formField,
}) {
this.$container = $(container); this.$container = $(container);
this.formField = formField; this.formField = formField;
this.environmentDropdownMap = new WeakMap(); this.environmentDropdownMap = new WeakMap();
@ -71,7 +68,7 @@ export default class VariableList {
this.initRow(rowEl); this.initRow(rowEl);
}); });
this.$container.on('click', '.js-row-remove-button', (e) => { this.$container.on('click', '.js-row-remove-button', e => {
e.preventDefault(); e.preventDefault();
this.removeRow($(e.currentTarget).closest('.js-row')); this.removeRow($(e.currentTarget).closest('.js-row'));
}); });
@ -81,7 +78,7 @@ export default class VariableList {
.join(','); .join(',');
// Remove any empty rows except the last row // Remove any empty rows except the last row
this.$container.on('blur', inputSelector, (e) => { this.$container.on('blur', inputSelector, e => {
const $row = $(e.currentTarget).closest('.js-row'); const $row = $(e.currentTarget).closest('.js-row');
if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) { if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
@ -136,7 +133,7 @@ export default class VariableList {
$rowClone.removeAttr('data-is-persisted'); $rowClone.removeAttr('data-is-persisted');
// Reset the inputs to their defaults // Reset the inputs to their defaults
Object.keys(this.inputMap).forEach((name) => { Object.keys(this.inputMap).forEach(name => {
const entry = this.inputMap[name]; const entry = this.inputMap[name];
$rowClone.find(entry.selector).val(entry.default); $rowClone.find(entry.selector).val(entry.default);
}); });
@ -171,7 +168,7 @@ export default class VariableList {
} }
checkIfRowTouched($row) { checkIfRowTouched($row) {
return Object.keys(this.inputMap).some((name) => { return Object.keys(this.inputMap).some(name => {
const entry = this.inputMap[name]; const entry = this.inputMap[name];
const $el = $row.find(entry.selector); const $el = $row.find(entry.selector);
return $el.length && $el.val() !== entry.default; return $el.length && $el.val() !== entry.default;
@ -190,11 +187,14 @@ export default class VariableList {
getAllData() { getAllData() {
// Ignore the last empty row because we don't want to try persist // Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems. // a blank variable and run into validation problems.
const validRows = this.$container.find('.js-row').toArray().slice(0, -1); const validRows = this.$container
.find('.js-row')
.toArray()
.slice(0, -1);
return validRows.map((rowEl) => { return validRows.map(rowEl => {
const resultant = {}; const resultant = {};
Object.keys(this.inputMap).forEach((name) => { Object.keys(this.inputMap).forEach(name => {
const entry = this.inputMap[name]; const entry = this.inputMap[name];
const $input = $(rowEl).find(entry.selector); const $input = $(rowEl).find(entry.selector);
if ($input.length) { if ($input.length) {
@ -207,11 +207,16 @@ export default class VariableList {
} }
getEnvironmentValues() { getEnvironmentValues() {
const valueMap = this.$container.find(this.inputMap.environment_scope.selector).toArray() const valueMap = this.$container
.reduce((prevValueMap, envInput) => ({ .find(this.inputMap.environment_scope.selector)
...prevValueMap, .toArray()
[envInput.value]: envInput.value, .reduce(
}), {}); (prevValueMap, envInput) => ({
...prevValueMap,
[envInput.value]: envInput.value,
}),
{},
);
return Object.keys(valueMap).map(createEnvironmentItem); return Object.keys(valueMap).map(createEnvironmentItem);
} }

View file

@ -2,10 +2,7 @@ import $ from 'jquery';
import VariableList from './ci_variable_list'; import VariableList from './ci_variable_list';
// Used for the variable list on scheduled pipeline edit page // Used for the variable list on scheduled pipeline edit page
export default function setupNativeFormVariableList({ export default function setupNativeFormVariableList({ container, formField = 'variables' }) {
container,
formField = 'variables',
}) {
const $container = $(container); const $container = $(container);
const variableList = new VariableList({ const variableList = new VariableList({

View file

@ -1,6 +1,6 @@
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import Vue from 'vue'; import Vue from 'vue';
import PersistentUserCallout from '../persistent_user_callout'; import initDismissableCallout from '~/dismissable_callout';
import { s__, sprintf } from '../locale'; import { s__, sprintf } from '../locale';
import Flash from '../flash'; import Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
@ -28,6 +28,7 @@ export default class Clusters {
installIngressPath, installIngressPath,
installRunnerPath, installRunnerPath,
installJupyterPath, installJupyterPath,
installKnativePath,
installPrometheusPath, installPrometheusPath,
managePrometheusPath, managePrometheusPath,
clusterStatus, clusterStatus,
@ -49,6 +50,7 @@ export default class Clusters {
installRunnerEndpoint: installRunnerPath, installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath, installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath, installJupyterEndpoint: installJupyterPath,
installKnativeEndpoint: installKnativePath,
}); });
this.installApplication = this.installApplication.bind(this); this.installApplication = this.installApplication.bind(this);
@ -62,7 +64,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token');
Clusters.initDismissableCallout(); initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels(); initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(); this.initApplications();
@ -105,12 +107,6 @@ export default class Clusters {
}); });
} }
static initDismissableCallout() {
const callout = document.querySelector('.js-cluster-security-warning');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
}
addListeners() { addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication); eventHub.$on('installApplication', this.installApplication);

View file

@ -1,15 +1,14 @@
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import setupToggleButtons from '~/toggle_buttons'; import setupToggleButtons from '~/toggle_buttons';
import PersistentUserCallout from '../persistent_user_callout'; import initDismissableCallout from '~/dismissable_callout';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
export default () => { export default () => {
const clusterList = document.querySelector('.js-clusters-list'); const clusterList = document.querySelector('.js-clusters-list');
const callout = document.querySelector('.gcp-signup-offer'); initDismissableCallout('.gcp-signup-offer');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
// The empty state won't have a clusterList // The empty state won't have a clusterList
if (clusterList) { if (clusterList) {

View file

@ -1,153 +1,162 @@
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue';
import { import {
APPLICATION_STATUS, APPLICATION_STATUS,
REQUEST_LOADING, REQUEST_LOADING,
REQUEST_SUCCESS, REQUEST_SUCCESS,
REQUEST_FAILURE, REQUEST_FAILURE,
} from '../constants'; } from '../constants';
export default { export default {
components: { components: {
loadingButton, loadingButton,
identicon, identicon,
},
props: {
id: {
type: String,
required: true,
}, },
props: { title: {
id: { type: String,
type: String, required: true,
required: true,
},
title: {
type: String,
required: true,
},
titleLink: {
type: String,
required: false,
},
manageLink: {
type: String,
required: false,
},
logoUrl: {
type: String,
required: false,
default: null,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
status: {
type: String,
required: false,
},
statusReason: {
type: String,
required: false,
},
requestStatus: {
type: String,
required: false,
},
requestReason: {
type: String,
required: false,
},
installApplicationRequestParams: {
type: Object,
required: false,
default: () => ({}),
},
}, },
computed: { titleLink: {
isUnknownStatus() { type: String,
return !this.isKnownStatus && this.status !== null; required: false,
}, },
isKnownStatus() { manageLink: {
return Object.values(APPLICATION_STATUS).includes(this.status); type: String,
}, required: false,
isInstalled() { },
return ( logoUrl: {
this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.UPDATED type: String,
); required: false,
}, default: null,
hasLogo() { },
return !!this.logoUrl; disabled: {
}, type: Boolean,
identiconId() { required: false,
// generate a deterministic integer id for the identicon background default: false,
return this.id.charCodeAt(0); },
}, status: {
rowJsClass() { type: String,
return `js-cluster-application-row-${this.id}`; required: false,
}, },
installButtonLoading() { statusReason: {
return !this.status || type: String,
this.status === APPLICATION_STATUS.SCHEDULED || required: false,
this.status === APPLICATION_STATUS.INSTALLING || },
this.requestStatus === REQUEST_LOADING; requestStatus: {
}, type: String,
installButtonDisabled() { required: false,
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but },
// we already made a request to install and are just waiting for the real-time requestReason: {
// to sync up. type: String,
return ((this.status !== APPLICATION_STATUS.INSTALLABLE required: false,
&& this.status !== APPLICATION_STATUS.ERROR) || },
installApplicationRequestParams: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
isUnknownStatus() {
return !this.isKnownStatus && this.status !== null;
},
isKnownStatus() {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
isInstalled() {
return (
this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED ||
this.status === APPLICATION_STATUS.UPDATING
);
},
hasLogo() {
return !!this.logoUrl;
},
identiconId() {
// generate a deterministic integer id for the identicon background
return this.id.charCodeAt(0);
},
rowJsClass() {
return `js-cluster-application-row-${this.id}`;
},
installButtonLoading() {
return (
!this.status ||
this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.INSTALLING ||
this.requestStatus === REQUEST_LOADING
);
},
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
return (
((this.status !== APPLICATION_STATUS.INSTALLABLE &&
this.status !== APPLICATION_STATUS.ERROR) ||
this.requestStatus === REQUEST_LOADING || this.requestStatus === REQUEST_LOADING ||
this.requestStatus === REQUEST_SUCCESS) && this.isKnownStatus; this.requestStatus === REQUEST_SUCCESS) &&
}, this.isKnownStatus
installButtonLabel() { );
let label; },
if ( installButtonLabel() {
this.status === APPLICATION_STATUS.NOT_INSTALLABLE || let label;
this.status === APPLICATION_STATUS.INSTALLABLE || if (
this.status === APPLICATION_STATUS.ERROR || this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.isUnknownStatus this.status === APPLICATION_STATUS.INSTALLABLE ||
) { this.status === APPLICATION_STATUS.ERROR ||
label = s__('ClusterIntegration|Install'); this.isUnknownStatus
} else if (this.status === APPLICATION_STATUS.SCHEDULED || ) {
this.status === APPLICATION_STATUS.INSTALLING) { label = s__('ClusterIntegration|Install');
label = s__('ClusterIntegration|Installing'); } else if (
} else if (this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.UPDATED) { this.status === APPLICATION_STATUS.INSTALLING
label = s__('ClusterIntegration|Installed'); ) {
} label = s__('ClusterIntegration|Installing');
} else if (
this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED ||
this.status === APPLICATION_STATUS.UPDATING
) {
label = s__('ClusterIntegration|Installed');
}
return label; return label;
},
showManageButton() {
return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
},
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() {
return this.status === APPLICATION_STATUS.ERROR ||
this.requestStatus === REQUEST_FAILURE;
},
generalErrorDescription() {
return sprintf(
s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title,
},
);
},
}, },
methods: { showManageButton() {
installClicked() { return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
eventHub.$emit('installApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
}, },
}; manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() {
return this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE;
},
generalErrorDescription() {
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title,
});
},
},
methods: {
installClicked() {
eventHub.$emit('installApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
},
};
</script> </script>
<template> <template>

View file

@ -1,12 +1,13 @@
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import helmInstallIllustration from '@gitlab-org/gitlab-svgs/illustrations/kubernetes-installation.svg'; import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png'; import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png'; import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png'; import helmLogo from 'images/cluster_app_logos/helm.png';
import jeagerLogo from 'images/cluster_app_logos/jeager.png'; import jeagerLogo from 'images/cluster_app_logos/jeager.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png'; import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png'; import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import knativeLogo from 'images/cluster_app_logos/knative.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png'; import meltanoLogo from 'images/cluster_app_logos/meltano.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png'; import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
@ -53,6 +54,7 @@ export default {
jeagerLogo, jeagerLogo,
jupyterhubLogo, jupyterhubLogo,
kubernetesLogo, kubernetesLogo,
knativeLogo,
meltanoLogo, meltanoLogo,
prometheusLogo, prometheusLogo,
}), }),
@ -136,6 +138,9 @@ export default {
jupyterHostname() { jupyterHostname() {
return this.applications.jupyter.hostname; return this.applications.jupyter.hostname;
}, },
knativeInstalled() {
return this.applications.knative.status === APPLICATION_STATUS.INSTALLED;
},
}, },
created() { created() {
this.helmInstallIllustration = helmInstallIllustration; this.helmInstallIllustration = helmInstallIllustration;
@ -321,7 +326,6 @@ export default {
:request-reason="applications.jupyter.requestReason" :request-reason="applications.jupyter.requestReason"
:install-application-request-params="{ hostname: applications.jupyter.hostname }" :install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled" :disabled="!helmInstalled"
class="hide-bottom-border rounded-bottom"
title-link="https://jupyterhub.readthedocs.io/en/stable/" title-link="https://jupyterhub.readthedocs.io/en/stable/"
> >
<div slot="description"> <div slot="description">
@ -371,6 +375,58 @@ export default {
</template> </template>
</div> </div>
</application-row> </application-row>
<application-row
id="knative"
:logo-url="knativeLogo"
:title="applications.knative.title"
:status="applications.knative.status"
:status-reason="applications.knative.statusReason"
:request-status="applications.knative.requestStatus"
:request-reason="applications.knative.requestReason"
:install-application-request-params="{ hostname: applications.knative.hostname}"
:disabled="!helmInstalled"
class="hide-bottom-border rounded-bottom"
title-link="https://github.com/knative/docs"
>
<div slot="description">
<p>
{{ s__(`ClusterIntegration|A Knative build extends Kubernetes
and utilizes existing Kubernetes primitives to provide you with
the ability to run on-cluster container builds from source.
For example, you can write a build that uses Kubernetes-native
resources to obtain your source code from a repository,
build it into container a image, and then run that image.`) }}
</p>
<template v-if="knativeInstalled">
<div class="form-group">
<label for="knative-domainname">
{{ s__('ClusterIntegration|Knative Domain Name:') }}
</label>
<input
id="knative-domainname"
v-model="applications.knative.hostname"
type="text"
class="form-control js-domainname"
readonly
/>
</div>
</template>
<template v-else>
<div class="form-group">
<label for="knative-domainname">
{{ s__('ClusterIntegration|Knative Domain Name:') }}
</label>
<input
id="knative-domainname"
v-model="applications.knative.hostname"
type="text"
class="form-control js-domainname"
/>
</div>
</template>
</div>
</application-row>
</div> </div>
</section> </section>
</template> </template>

View file

@ -6,6 +6,7 @@ export const APPLICATION_STATUS = {
INSTALLING: 'installing', INSTALLING: 'installing',
INSTALLED: 'installed', INSTALLED: 'installed',
UPDATED: 'updated', UPDATED: 'updated',
UPDATING: 'updating',
ERROR: 'errored', ERROR: 'errored',
}; };
@ -15,3 +16,4 @@ export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure'; export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress'; export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter'; export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';

View file

@ -9,6 +9,7 @@ export default class ClusterService {
runner: this.options.installRunnerEndpoint, runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint, prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint, jupyter: this.options.installJupyterEndpoint,
knative: this.options.installKnativeEndpoint,
}; };
} }

View file

@ -1,5 +1,5 @@
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import { INGRESS, JUPYTER } from '../constants'; import { INGRESS, JUPYTER, KNATIVE } from '../constants';
export default class ClusterStore { export default class ClusterStore {
constructor() { constructor() {
@ -46,6 +46,14 @@ export default class ClusterStore {
requestReason: null, requestReason: null,
hostname: null, hostname: null,
}, },
knative: {
title: s__('ClusterIntegration|Knative'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
hostname: null,
},
}, },
}; };
} }
@ -76,12 +84,8 @@ export default class ClusterStore {
this.state.status = serverState.status; this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason; this.state.statusReason = serverState.status_reason;
serverState.applications.forEach((serverAppEntry) => { serverState.applications.forEach(serverAppEntry => {
const { const { name: appId, status, status_reason: statusReason } = serverAppEntry;
name: appId,
status,
status_reason: statusReason,
} = serverAppEntry;
this.state.applications[appId] = { this.state.applications[appId] = {
...(this.state.applications[appId] || {}), ...(this.state.applications[appId] || {}),
@ -97,6 +101,9 @@ export default class ClusterStore {
(this.state.applications.ingress.externalIp (this.state.applications.ingress.externalIp
? `jupyter.${this.state.applications.ingress.externalIp}.nip.io` ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
: ''); : '');
} else if (appId === KNATIVE) {
this.state.applications.knative.hostname =
serverAppEntry.hostname || this.state.applications.knative.hostname;
} }
}); });
} }

View file

@ -24,36 +24,44 @@ class CommentTypeToggle {
setConfig() { setConfig() {
const config = { const config = {
InputSetter: [{ InputSetter: [
input: this.noteTypeInput, {
valueAttribute: 'data-value', input: this.noteTypeInput,
}, valueAttribute: 'data-value',
{ },
input: this.submitButton, {
valueAttribute: 'data-submit-text', input: this.submitButton,
}], valueAttribute: 'data-submit-text',
},
],
}; };
if (this.closeButton) { if (this.closeButton) {
config.InputSetter.push({ config.InputSetter.push(
input: this.closeButton, {
valueAttribute: 'data-close-text', input: this.closeButton,
}, { valueAttribute: 'data-close-text',
input: this.closeButton, },
valueAttribute: 'data-close-text', {
inputAttribute: 'data-alternative-text', input: this.closeButton,
}); valueAttribute: 'data-close-text',
inputAttribute: 'data-alternative-text',
},
);
} }
if (this.reopenButton) { if (this.reopenButton) {
config.InputSetter.push({ config.InputSetter.push(
input: this.reopenButton, {
valueAttribute: 'data-reopen-text', input: this.reopenButton,
}, { valueAttribute: 'data-reopen-text',
input: this.reopenButton, },
valueAttribute: 'data-reopen-text', {
inputAttribute: 'data-alternative-text', input: this.reopenButton,
}); valueAttribute: 'data-reopen-text',
inputAttribute: 'data-alternative-text',
},
);
} }
return config; return config;

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