New upstream version 11.8.0
This commit is contained in:
parent
591efccd8b
commit
0329642ba5
2015 changed files with 177081 additions and 29694 deletions
|
@ -18,6 +18,7 @@ const plugins = [
|
|||
'@babel/plugin-syntax-import-meta',
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'@babel/plugin-proposal-json-strings',
|
||||
'@babel/plugin-proposal-private-methods',
|
||||
];
|
||||
|
||||
// add code coverage tooling if necessary
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -80,3 +80,4 @@ eslint-report.html
|
|||
package-lock.json
|
||||
/junit_*.xml
|
||||
/coverage-frontend/
|
||||
jsdoc/
|
||||
|
|
141
.gitlab-ci.yml
141
.gitlab-ci.yml
|
@ -1,4 +1,4 @@
|
|||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.9-git-2.18-chrome-69.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
|
||||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
|
||||
|
||||
.dedicated-runner: &dedicated-runner
|
||||
retry: 1
|
||||
|
@ -120,9 +120,8 @@ stages:
|
|||
variables: &single-script-job-variables
|
||||
GIT_STRATEGY: none
|
||||
before_script:
|
||||
# We need to download the script rather than clone the repo since the
|
||||
# package-and-qa job will not be able to run when the branch gets
|
||||
# deleted (when merging the MR).
|
||||
# We don't clone the repo by using GIT_STRATEGY: none and only download the
|
||||
# single script we need here so it's much faster than cloning.
|
||||
- export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}"
|
||||
- apk add --update openssl
|
||||
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME
|
||||
|
@ -228,20 +227,21 @@ stages:
|
|||
# Trigger a package build in omnibus-gitlab repository
|
||||
#
|
||||
package-and-qa:
|
||||
<<: *single-script-job
|
||||
image: ruby:2.5-alpine
|
||||
stage: test
|
||||
before_script: []
|
||||
dependencies: []
|
||||
cache: {}
|
||||
variables:
|
||||
<<: *single-script-job-variables
|
||||
GIT_DEPTH: "1"
|
||||
API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
|
||||
SCRIPT_NAME: trigger-build
|
||||
retry: 0
|
||||
script:
|
||||
- gem install gitlab --no-document
|
||||
- apk add --update openssl curl jq
|
||||
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/review_apps/review-apps.sh
|
||||
- chmod 755 review-apps.sh
|
||||
- source ./review-apps.sh
|
||||
- gem install gitlab --no-document
|
||||
- source ./scripts/review_apps/review-apps.sh
|
||||
- wait_for_job_to_be_done "gitlab:assets:compile"
|
||||
- ./$SCRIPT_NAME omnibus
|
||||
- ./scripts/trigger-build omnibus
|
||||
when: manual
|
||||
only:
|
||||
- //@gitlab-org/gitlab-ce
|
||||
|
@ -386,20 +386,27 @@ flaky-examples-check:
|
|||
- scripts/merge-reports ${NEW_FLAKY_SPECS_REPORT} rspec_flaky/new_*_*.json
|
||||
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
|
||||
|
||||
.assets-compile-cache: &assets-compile-cache
|
||||
cache:
|
||||
key: "assets-compile:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v4"
|
||||
paths:
|
||||
- vendor/ruby/
|
||||
- .yarn-cache/
|
||||
# We have disabled caching of sprockets for now, as it fails to pick up changes in SCSS:
|
||||
# https://gitlab.com/gitlab-org/gitlab-ce/issues/57431
|
||||
# - tmp/cache/assets/sprockets
|
||||
|
||||
compile-assets:
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
<<: *use-pg
|
||||
stage: prepare
|
||||
cache:
|
||||
<<: *default-cache
|
||||
script:
|
||||
- node --version
|
||||
- date
|
||||
- yarn install --frozen-lockfile --cache-folder .yarn-cache
|
||||
- date
|
||||
- free -m
|
||||
- bundle exec rake gitlab:assets:compile
|
||||
- scripts/clean-old-cached-assets
|
||||
variables:
|
||||
# we override the max_old_space_size to prevent OOM errors
|
||||
NODE_OPTIONS: --max_old_space_size=3584
|
||||
|
@ -408,6 +415,7 @@ compile-assets:
|
|||
paths:
|
||||
- node_modules
|
||||
- public/assets
|
||||
<<: *assets-compile-cache
|
||||
|
||||
setup-test-env:
|
||||
<<: *dedicated-runner
|
||||
|
@ -427,15 +435,7 @@ setup-test-env:
|
|||
- vendor/gitaly-ruby
|
||||
|
||||
# GitLab Review apps
|
||||
.review-base: &review-base
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
|
||||
stage: test
|
||||
cache: {}
|
||||
dependencies: []
|
||||
environment: &review-environment
|
||||
name: review/${CI_COMMIT_REF_NAME}
|
||||
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
|
||||
.review-only: &review-only
|
||||
only:
|
||||
refs:
|
||||
- branches@gitlab-org/gitlab-ce
|
||||
|
@ -445,6 +445,17 @@ setup-test-env:
|
|||
refs:
|
||||
- master
|
||||
- /(^docs[\/-].*|.*-docs$)/
|
||||
|
||||
.review-base: &review-base
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
<<: *review-only
|
||||
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
|
||||
stage: test
|
||||
cache: {}
|
||||
dependencies: []
|
||||
environment: &review-environment
|
||||
name: review/${CI_COMMIT_REF_NAME}
|
||||
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
|
||||
before_script: []
|
||||
|
||||
.review-docker: &review-docker
|
||||
|
@ -499,6 +510,22 @@ rspec-mysql:
|
|||
<<: *rspec-metadata-mysql
|
||||
parallel: 50
|
||||
|
||||
.rspec-quarantine: &rspec-quarantine
|
||||
script:
|
||||
- export CACHE_CLASSES=true
|
||||
- scripts/gitaly-test-spawn
|
||||
- bin/rspec --color --format documentation --tag quarantine spec/
|
||||
|
||||
rspec-pg-quarantine:
|
||||
<<: *rspec-metadata-pg
|
||||
<<: *rspec-quarantine
|
||||
allow_failure: true
|
||||
|
||||
rspec-mysql-quarantine:
|
||||
<<: *rspec-metadata-mysql
|
||||
<<: *rspec-quarantine
|
||||
allow_failure: true
|
||||
|
||||
static-analysis:
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
dependencies:
|
||||
|
@ -527,7 +554,7 @@ docs lint:
|
|||
script:
|
||||
- scripts/lint-doc.sh
|
||||
- scripts/lint-changelog-yaml
|
||||
- mv doc/ /tmp/gitlab-docs/content/
|
||||
- mv doc/ /tmp/gitlab-docs/content/$DOCS_GITLAB_REPO_SUFFIX
|
||||
- cd /tmp/gitlab-docs
|
||||
# Build HTML from Markdown
|
||||
- bundle exec nanoc
|
||||
|
@ -608,8 +635,9 @@ gitlab:setup-mysql:
|
|||
# Frontend-related jobs
|
||||
gitlab:assets:compile:
|
||||
<<: *dedicated-no-docs-pull-cache-job
|
||||
image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-69.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1
|
||||
dependencies: []
|
||||
image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-71.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1
|
||||
dependencies:
|
||||
- setup-test-env
|
||||
services:
|
||||
- docker:stable-dind
|
||||
variables:
|
||||
|
@ -623,18 +651,19 @@ gitlab:assets:compile:
|
|||
DOCKER_DRIVER: overlay2
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
script:
|
||||
- date
|
||||
- node --version
|
||||
- yarn install --frozen-lockfile --production --cache-folder .yarn-cache
|
||||
- date
|
||||
- free -m
|
||||
- bundle exec rake gitlab:assets:compile
|
||||
- scripts/build_assets_image
|
||||
- time scripts/build_assets_image
|
||||
- scripts/clean-old-cached-assets
|
||||
artifacts:
|
||||
name: webpack-report
|
||||
expire_in: 31d
|
||||
paths:
|
||||
- webpack-report/
|
||||
- public/assets/
|
||||
<<: *assets-compile-cache
|
||||
only:
|
||||
- //@gitlab-org/gitlab-ce
|
||||
- //@gitlab-org/gitlab-ee
|
||||
|
@ -788,6 +817,7 @@ qa:selectors:
|
|||
- bundle exec bin/qa Test::Sanity::Selectors
|
||||
|
||||
.qa-frontend-node: &qa-frontend-node
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
stage: test
|
||||
variables:
|
||||
NODE_OPTIONS: --max_old_space_size=3584
|
||||
|
@ -802,11 +832,6 @@ qa:selectors:
|
|||
- yarn install --frozen-lockfile --cache-folder .yarn-cache
|
||||
- date
|
||||
- yarn run webpack-prod
|
||||
<<: *except-docs
|
||||
|
||||
qa-frontend-node:6:
|
||||
<<: *qa-frontend-node
|
||||
image: node:6-alpine
|
||||
|
||||
qa-frontend-node:8:
|
||||
<<: *qa-frontend-node
|
||||
|
@ -854,6 +879,21 @@ lint:javascript:report:
|
|||
paths:
|
||||
- eslint-report.html
|
||||
|
||||
jsdoc:
|
||||
<<: *dedicated-no-docs-pull-cache-job
|
||||
stage: post-test
|
||||
dependencies:
|
||||
- compile-assets
|
||||
before_script: []
|
||||
script:
|
||||
- date
|
||||
- yarn run jsdoc || true # ignore exit code
|
||||
artifacts:
|
||||
name: jsdoc
|
||||
expire_in: 31d
|
||||
paths:
|
||||
- jsdoc/
|
||||
|
||||
pages:
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
before_script: []
|
||||
|
@ -863,6 +903,7 @@ pages:
|
|||
- karma
|
||||
- gitlab:assets:compile
|
||||
- lint:javascript:report
|
||||
- jsdoc
|
||||
script:
|
||||
- mv public/ .public/
|
||||
- mkdir public/
|
||||
|
@ -872,6 +913,7 @@ pages:
|
|||
- mv webpack-report/ public/webpack-report/ || true
|
||||
- cp .public/assets/application-*.css public/application.css || true
|
||||
- cp .public/assets/application-*.css.gz public/application.css.gz || true
|
||||
- mv jsdoc/ public/jsdoc/ || true
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
|
@ -918,6 +960,23 @@ no_ee_check:
|
|||
- //@gitlab-org/gitlab-ce
|
||||
|
||||
# GitLab Review apps
|
||||
review-build-cng:
|
||||
<<: *review-only
|
||||
image: ruby:2.5-alpine
|
||||
stage: test
|
||||
before_script: []
|
||||
dependencies: []
|
||||
cache: {}
|
||||
variables:
|
||||
GIT_DEPTH: "1"
|
||||
API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
|
||||
script:
|
||||
- apk add --update openssl curl jq
|
||||
- gem install gitlab --no-document
|
||||
- source ./scripts/review_apps/review-apps.sh
|
||||
- wait_for_job_to_be_done "gitlab:assets:compile"
|
||||
- BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
|
||||
|
||||
review-deploy:
|
||||
<<: *review-base
|
||||
retry: 2
|
||||
|
@ -932,15 +991,14 @@ review-deploy:
|
|||
<<: *review-environment
|
||||
on_stop: review-stop
|
||||
before_script:
|
||||
- apk update && apk add jq
|
||||
- gem install gitlab --no-document
|
||||
script:
|
||||
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
|
||||
- export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
|
||||
- export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
|
||||
- apk update && apk add jq
|
||||
- gem install gitlab --no-document
|
||||
- source ./scripts/review_apps/review-apps.sh
|
||||
- wait_for_job_to_be_done "gitlab:assets:compile"
|
||||
- BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
|
||||
script:
|
||||
- wait_for_job_to_be_done "review-build-cng"
|
||||
- check_kube_domain
|
||||
- download_gitlab_chart
|
||||
- ensure_namespace
|
||||
|
@ -951,7 +1009,6 @@ review-deploy:
|
|||
|
||||
.review-qa-base: &review-qa-base
|
||||
<<: *review-docker
|
||||
retry: 2
|
||||
allow_failure: true
|
||||
variables:
|
||||
<<: *review-docker-variables
|
||||
|
|
16
.gitlab/issue_templates/Coding style proposal.md
Normal file
16
.gitlab/issue_templates/Coding style proposal.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
## Description of the proposal
|
||||
|
||||
<!--
|
||||
Please describe the proposal and add a link to the source (for example, http://www.betterspecs.org/).
|
||||
-->
|
||||
|
||||
- [ ] Mention the proposal in the next backend weekly call and the #backend channel to encourage contribution
|
||||
- [ ] Proceed with the proposal once 50% of the maintainers have weighed in, and 80% of the votes are :+1:
|
||||
- [ ] Once approved, mention it again in the next backend weekly call and the #backend channel
|
||||
|
||||
|
||||
/label ~"development guidelines"
|
||||
/label ~"Style decision"
|
||||
/label ~Documentation
|
||||
|
||||
/cc @gitlab-org/maintainers/rails-backend
|
|
@ -4,7 +4,30 @@
|
|||
|
||||
### Target audience
|
||||
|
||||
<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst" -->
|
||||
<!--- For whom are we doing this? Include a [persona](https://design.gitlab.com/research/personas)
|
||||
listed below, if applicable, along with its [label](https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A),
|
||||
or define a specific company role, e.g. "Release Manager".
|
||||
|
||||
Existing personas are: (copy relevant personas out of this comment, and delete any persona that does not apply)
|
||||
|
||||
- Parker, Product Manager, https://design.gitlab.com/research/personas#persona-parker
|
||||
/label ~"Persona: Product Manager"
|
||||
|
||||
- Delaney, Development Team Lead, https://design.gitlab.com/research/personas#persona-delaney
|
||||
/label ~"Persona: Development Team Lead"
|
||||
|
||||
- Sasha, Software Developer, https://design.gitlab.com/research/personas#persona-sasha
|
||||
/label ~"Persona: Software developer"
|
||||
|
||||
- Devon, DevOps Engineer, https://design.gitlab.com/research/personas#persona-devon
|
||||
/label ~"Persona: DevOps Engineer"
|
||||
|
||||
- Sidney, Systems Administrator, https://design.gitlab.com/research/personas#persona-sidney
|
||||
/label ~"Persona: Systems Administrator"
|
||||
|
||||
- Sam, Security Analyst, https://design.gitlab.com/research/personas#persona-sam
|
||||
/label ~"Persona: Security Analyst"
|
||||
-->
|
||||
|
||||
### Further details
|
||||
|
||||
|
@ -12,12 +35,12 @@
|
|||
|
||||
### Proposal
|
||||
|
||||
<!--- How are we going to solve the problem? -->
|
||||
<!--- How are we going to solve the problem? Try to include the user journey! -->
|
||||
|
||||
### What does success look like, and how can we measure that?
|
||||
|
||||
<!--- If no way to measure success, link to an issue that will implement a way to measure this -->
|
||||
<!--- Define both the success metrics and acceptance criteria. Note that success metrics indicate the desired business outcomes, while acceptance criteria indicate when the solution is working correctly. If there is no way to measure success, link to an issue that will implement a way to measure this -->
|
||||
|
||||
### Links / references
|
||||
|
||||
/label ~"feature proposal"
|
||||
/label ~feature
|
||||
|
|
69
.gitlab/issue_templates/Security Release.md
Normal file
69
.gitlab/issue_templates/Security Release.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
<!--
|
||||
# Read me first!
|
||||
|
||||
Set the title to: `Security Release: 11.4.X, 11.3.X, and 11.2.X`
|
||||
-->
|
||||
|
||||
## Releases tasks
|
||||
|
||||
- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/release-manager.md
|
||||
- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
|
||||
- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/security-engineer.md
|
||||
|
||||
## Version issues:
|
||||
|
||||
* 11.4.X: {release task link}
|
||||
* 11.3.X: {release task link}
|
||||
* 11.2.X: {release task link}
|
||||
|
||||
## Security Issues:
|
||||
|
||||
### CE
|
||||
|
||||
* {https://gitlab.com/gitlab-org/gitlab-ce/issues link}
|
||||
|
||||
### EE
|
||||
|
||||
* {https://gitlab.com/gitlab-org/gitlab-ee/issues link}
|
||||
|
||||
## Security Issues in dev.gitlab.org:
|
||||
|
||||
### CE
|
||||
|
||||
- {https://dev.gitlab.org/gitlab/gitlabhq/issues link}
|
||||
|
||||
| Version | MR |
|
||||
|---------|----|
|
||||
| 11.4 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} |
|
||||
| 11.3 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} |
|
||||
| 11.2 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} |
|
||||
| master | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} |
|
||||
|
||||
|
||||
|
||||
### EE
|
||||
|
||||
* {https://dev.gitlab.org/gitlab/gitlabhq/issues/ link}
|
||||
|
||||
|
||||
| Version | MR |
|
||||
|---------|----|
|
||||
| 11.4| {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} |
|
||||
| 11.3 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} |
|
||||
| 11.2 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} |
|
||||
| master | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} |
|
||||
|
||||
|
||||
## QA
|
||||
{QA issue link}
|
||||
|
||||
## Blog post
|
||||
|
||||
Dev: {https://dev.gitlab.org/gitlab/www-gitlab-com/merge_requests/ link}<br/>
|
||||
gitlab.com: {https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/ link}
|
||||
|
||||
## Email notification
|
||||
{https://gitlab.com/gitlab-com/marketing/general/issues/ link}
|
||||
|
||||
/label ~security
|
||||
/confidential
|
|
@ -3,30 +3,26 @@
|
|||
|
||||
Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
|
||||
|
||||
Set the title to: `[Security] Description of the original issue`
|
||||
Set the title to: `Description of the original issue`
|
||||
-->
|
||||
|
||||
### Prior to the security release
|
||||
### Prior to starting the security release work
|
||||
|
||||
- [ ] Read the [security process for developers] if you are not familiar with it.
|
||||
- [ ] Link to the original issue adding it to the [links section](#links)
|
||||
- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
|
||||
- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
|
||||
- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
|
||||
- [ ] Add a link to the MR to the [links section](#links)
|
||||
- [ ] 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**.
|
||||
- [ ] Add a link to this issue on the original security issue.
|
||||
- [ ] Create a new branch prefixing it with `security-`
|
||||
- [ ] Create a MR targeting `dev.gitlab.org` `master`
|
||||
- [ ] Add a link to this issue in the original security issue on `gitlab.com`.
|
||||
|
||||
#### Backports
|
||||
|
||||
- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
|
||||
- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases, plus the current RC if between the 7th and 22nd of the month.
|
||||
- [ ] At this point, it might be easy to squash the commits from the MR into one
|
||||
- You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation]
|
||||
- [ ] 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`
|
||||
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
|
||||
- [ ] Add the ~"Merge into Security" label to all of the MRs.
|
||||
- [ ] Create each MR targetting the stable branch `X-Y-stable`, using the "Security Release" merge request template.
|
||||
- Every merge request will have its own set of TODOs, so make sure to
|
||||
complete those.
|
||||
- [ ] 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
|
||||
|
|
31
.gitlab/merge_request_templates/Security Release.md
Normal file
31
.gitlab/merge_request_templates/Security Release.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!--
|
||||
# README first!
|
||||
This MR should be created on `dev.gitlab.org`.
|
||||
|
||||
See [the general developer security release guidelines](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md).
|
||||
|
||||
This merge request _must not_ close the corresponding security issue _unless_ it
|
||||
targets master.
|
||||
|
||||
-->
|
||||
## Related issues
|
||||
|
||||
<!-- Mention the issue(s) this MR is related to -->
|
||||
|
||||
## Developer checklist
|
||||
|
||||
- [ ] Link to the developer security workflow issue on `dev.gitlab.org`
|
||||
- [ ] MR targets `master`, or `X-Y-stable` for backports
|
||||
- [ ] Milestone is set for the version this MR applies to
|
||||
- [ ] Title of this MR is the same as for all backports
|
||||
- [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security`
|
||||
- [ ] Add a link to this MR in the `links` section of related issue
|
||||
- [ ] Add a link to an EE MR if required
|
||||
- [ ] Assign to a reviewer
|
||||
|
||||
## Reviewer checklist
|
||||
|
||||
- [ ] Correct milestone is applied and the title is matching across all backports
|
||||
- [ ] Assigned to `@gitlab-release-tools-bot` with passing CI pipelines
|
||||
|
||||
/label ~security
|
|
@ -143,6 +143,7 @@ Naming/FileName:
|
|||
- XMPP
|
||||
- XSRF
|
||||
- XSS
|
||||
- GRPC
|
||||
|
||||
# GitLab ###################################################################
|
||||
|
||||
|
|
|
@ -15,12 +15,6 @@ Capybara/CurrentPathExpectation:
|
|||
Layout/EmptyLinesAroundArguments:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 253
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
|
||||
Layout/ExtraSpacing:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 83
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
|
@ -86,11 +80,6 @@ Lint/InterpolationCheck:
|
|||
Lint/MissingCopEnableDirective:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
Lint/ReturnInVoidContext:
|
||||
Exclude:
|
||||
- 'app/models/project.rb'
|
||||
|
||||
# Offense count: 9
|
||||
Lint/UriEscapeUnescape:
|
||||
Exclude:
|
||||
|
@ -443,11 +432,6 @@ Style/LineEndConcatenation:
|
|||
- 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
|
||||
- 'spec/lib/gitlab/incoming_email_spec.rb'
|
||||
|
||||
# Offense count: 39
|
||||
# Cop supports --auto-correct.
|
||||
Style/MethodCallWithoutArgsParentheses:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 18
|
||||
Style/MethodMissing:
|
||||
Enabled: false
|
||||
|
@ -686,17 +670,6 @@ Style/TrailingUnderscoreVariable:
|
|||
- 'spec/lib/gitlab/etag_caching/middleware_spec.rb'
|
||||
- 'spec/services/quick_actions/interpret_service_spec.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist.
|
||||
# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym
|
||||
Style/TrivialAccessors:
|
||||
Exclude:
|
||||
- 'app/models/external_issue.rb'
|
||||
- 'app/serializers/base_serializer.rb'
|
||||
- 'lib/gitlab/auth/ldap/person.rb'
|
||||
- 'lib/system_check/base_check.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Cop supports --auto-correct.
|
||||
Style/UnlessElse:
|
||||
|
|
322
CHANGELOG.md
322
CHANGELOG.md
|
@ -2,6 +2,253 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 11.8.0 (2019-02-22)
|
||||
|
||||
### Security (7 changes, 1 of them is from the community)
|
||||
|
||||
- Sanitize user full name to clean up any URL to prevent mail clients from auto-linking URLs. !2793
|
||||
- Update Helm to 2.12.2 to address Helm client vulnerability. !24418 (Takuya Noguchi)
|
||||
- Use sanitized user status message for user popover.
|
||||
- Validate bundle files before unpacking them.
|
||||
- Alias GitHub and BitBucket OAuth2 callback URLs.
|
||||
- Fixed XSS content in KaTex links.
|
||||
- Disallows unauthorized users from accessing the pipelines section.
|
||||
|
||||
### Removed (2 changes, 1 of them is from the community)
|
||||
|
||||
- Removed deprecated Redcarpet markdown engine.
|
||||
- Remove Cancel all jobs button in general jobs list view. (Jordi Llull)
|
||||
|
||||
### Fixed (84 changes, 20 of them are from the community)
|
||||
|
||||
- Fix ambiguous brackets in task lists. !18514 (Jared Deckard <jared.deckard@gmail.com>)
|
||||
- Fix lost line number when navigating to a specific line in a protected file before authenticating. !19165 (Scott Escue)
|
||||
- Fix suboptimal handling of checkbox and radio input events causing group general settings submit button to stay disabled after changing its visibility. !23022
|
||||
- Fix upcoming milestones filter not including group milestones. !23098 (Heinrich Lee Yu)
|
||||
- Update runner admin page to make description field larger. !23593 (Sascha Reynolds)
|
||||
- Fix Bitbucket Server import not allowing personal projects. !23601
|
||||
- Fix bug causing repository mirror settings UI to break. !23712
|
||||
- Fix foreground color for labels to ensure consistency of label appearance. !23873 (Nathan Friend)
|
||||
- Resolve In Merge Request diff screen, master is not a hyperlink. !23874
|
||||
- Show the correct error page when access is denied. !23932
|
||||
- Increase reliability and performance of toggling task items. !23938
|
||||
- Modify file restore to rectify tar issue. !24000
|
||||
- Fix default visibility_level for new projects. !24120 (Fabian Schneider @fabsrc)
|
||||
- Footnotes now render properly in markdown. !24168
|
||||
- Emoji and cancel button are taller than input in set user status modal. !24173 (Dhiraj Bodicherla)
|
||||
- Adjusts duplicated line when commenting on unfolded diff lines (in the bottom). !24201
|
||||
- Adjust height of "Add list" dropdown in issue boards. !24227
|
||||
- Improves restriction of multiple Kubernetes clusters through API. !24251
|
||||
- Fix files/blob api endpoints content disposition. !24267
|
||||
- Cleanup stale +deleted repo paths on project removal (adjusts project removal bug). !24269
|
||||
- Handle regular job dependencies next to parallelized job dependencies. !24273
|
||||
- Proper align Projects dropdown on issue boards page. !24277 (Johann Hubert Sonntagbauer)
|
||||
- Resolve When merging an MR, the squash checkbox isnt always supported. !24296
|
||||
- Fix Bitbucket Server importer error handling. !24343
|
||||
- Fix syntax highlighting for suggested changes preview. !24358
|
||||
- API: Support dots in wiki slugs. !24383 (Robert Schilling)
|
||||
- Show CI artifact file size with 3 significant digits on 'browse job artifacts' page. !24387
|
||||
- API: Support username with dots. !24395 (Robert Schilling)
|
||||
- API: Fix default_branch_protection admin setting. !24398 (Robert Schilling)
|
||||
- Remove unwanted margin above suggested changes. !24419
|
||||
- Prevent checking protected_ref? for ambiguous refs. !24437
|
||||
- Update metrics environment dropdown to show complete option set. !24441
|
||||
- Fix empty labels of CI builds for gitlab-pages on pipeline page. !24451
|
||||
- Do not run spam checks on confidential issues. !24453
|
||||
- Upgrade KaTeX to version 0.10.0. !24478 (Andrew Harmon)
|
||||
- Avoid overwriting default jaeger values with nil. !24482
|
||||
- Display SAML failure messages instead of expecting CSRF token. !24509
|
||||
- Adjust vertical alignment for project visibility icons. !24511 (Martin Hobert)
|
||||
- Load initUserInternalRegexPlaceholder only when required. !24522
|
||||
- Hashed Storage: `AfterRenameService` was receiving the wrong `old_path` under some circunstances. !24526
|
||||
- Resolve Runners IPv6 address overlaps other values. !24531
|
||||
- Fix 404s with snippet uploads in object storage. !24550
|
||||
- Fixed oversized custom project notification selector dropdown. !24557
|
||||
- Allow users with full private access to read private personal snippets. !24560
|
||||
- Resolve Pipeline stages job action button icon is not aligned. !24577
|
||||
- Fix cluster page non-interactive on form validation error. !24583
|
||||
- Fix 404s for snippet uploads when relative URL root used. !24588
|
||||
- Fix markdown table border. !24601
|
||||
- Fix CSS grid on a new Project/Group Milestone. !24614 (Takuya Noguchi)
|
||||
- Prevent unload when Recaptcha is open. !24625
|
||||
- Clean up unicorn sampler metric labels. !24626 (bjk-gitlab)
|
||||
- Support bamboo api polymorphism. !24680 (Alex Lossent)
|
||||
- Ensure Cert Manager works with Auto DevOps URLs greater than 64 bytes. !24683
|
||||
- Fix failed LDAP logins when nil user_id present. !24749
|
||||
- fix display comment avatars issue in IE 11. !24777 (Gokhan Apaydin)
|
||||
- Fix template labels not being created on new projects. !24803
|
||||
- Fix cluster installation processing spinner. !24814
|
||||
- Append prioritized label before pagination. !24815
|
||||
- Resolve UI bug adding group members with lower permissions. !24820
|
||||
- Make `ActionController::Parameters` serializable for sidekiq jobs. !24864
|
||||
- Fix Jira Service password validation on project integration services. !24896 (Daniel Juarez)
|
||||
- Fix potential Addressable::URI::InvalidURIError. !24908
|
||||
- Update Workhorse to v8.2.0. !24909
|
||||
- Encode Content-Disposition filenames. !24919
|
||||
- Avoid race conditions when creating GpgSignature. !24939
|
||||
- Create the source branch for a GitHub import. !25064
|
||||
- Fix suggested changes syntax highlighting. !25116
|
||||
- Fix counts in milestones dashboard. !25230
|
||||
- Fixes incorrect TLD validation errors for Kubernetes cluster domain. !25262
|
||||
- Fix 403 errors when adding an assignee list in project boards. !25263
|
||||
- Prevent Auto DevOps from trying to deploy without a domain name. !25308
|
||||
- Fix uninitialized constant with GitLab Pages.
|
||||
- Increase line height of project summaries. (gfyoung)
|
||||
- Remove extra space between MR tab bar and sticky file headers.
|
||||
- Correct spacing for comparison page.
|
||||
- Update CI YAML param table with include.
|
||||
- Return bottom border on MR Tabs.
|
||||
- Fixes z-index and margins of archived alert in job page.
|
||||
- Fixes archived sticky top bar without perfomance bar.
|
||||
- Fixed rebase button not showing in merge request widget.
|
||||
- Fixed double tooltips on note awards buttons.
|
||||
- Allow suggestions to be copied and pasted as GFM.
|
||||
- Fix bug that caused Suggestion Markdown toolbar button to insert snippet with leading +/-/<space>.
|
||||
- Moved primary button for labels to follow the design patterns used on rest of the site. (Martin Hobert)
|
||||
|
||||
### Changed (37 changes, 11 of them are from the community)
|
||||
|
||||
- Change spawning of tooltips to be top by default. !21223
|
||||
- Standardize filter value capitlization in filter bar in both issues and boards pages. !23846 (obahareth)
|
||||
- Refresh group overview to match project overview. !23866
|
||||
- Build number does not need to be tweaked anymore for the TeamCity integration to work properly. !23898
|
||||
- Added empty project illustration and updated text to user profile overview. !23973 (Fernando Arias)
|
||||
- Modified Knative list view to provide more details. !24072 (Chris Baumbauer)
|
||||
- Move cancel & new issue button on job page. !24074
|
||||
- Make issuable empty states actionable. !24077
|
||||
- Fix code search when text is larger than max gRPC message size. !24111
|
||||
- Update string structure for available group runners. !24187 (George Tsiolis)
|
||||
- Remove multilingual translation from the word "in" in the job details sidebar. !24192 (Nathan Friend)
|
||||
- Fix duplicate project disk path in BackfillLegacyProjectRepositories. !24213
|
||||
- Ensured links to a comment or system note anchor resolves to the right note if a user has a discussion filter. !24228
|
||||
- Remove expansion hover animation from pipeline status icon buttons. !24268 (Nathan Friend)
|
||||
- Redesigned related merge requests in issue page. !24270
|
||||
- Return the maximum group access level in the projects API. !24403
|
||||
- Update project topics styling to use badges design. !24415
|
||||
- Display "commented" only for commit discussions on merge requests. !24427
|
||||
- Upgrade js-regex gem to version 3.1. !24433 (rroger)
|
||||
- Prevent Sidekiq arguments over 10 KB in size from being logged to JSON. !24493
|
||||
- Added Avatar in the settings sidebar. !24515 (Yoginth)
|
||||
- Refresh empty states for profile page tabs. !24549
|
||||
- remove red/green colors from diff view of no-color syntax theme. !24582 (khm)
|
||||
- Get remote IP address of runner. !24624
|
||||
- Update last_activity_on for Users on some main GET endpoints. !24642
|
||||
- Update metrics dashboard graph design. !24653
|
||||
- Update to GitLab SVG icon from Font Awesome in profile for location and work. !24671 (Yoginth)
|
||||
- Add template for Android with Fastlane. !24722
|
||||
- Display timestamps to messages printed by gitlab:backup:restore rake tasks. (Will Chandler)
|
||||
- Show MR statistics in diff comparisons.
|
||||
- Make possible to toggle file tree while scrolling through diffs.
|
||||
- Use delete instead of remove when referring to `git branch -D`.
|
||||
- Add folder header to files in merge request tree list.
|
||||
- Added fuzzy file finder to merge requests.
|
||||
- Collapse directory structure in merge request file tree.
|
||||
- Adds skeleton loading to releases page.
|
||||
- Support multiple outputs in jupyter notebooks.
|
||||
|
||||
### Performance (8 changes, 1 of them is from the community)
|
||||
|
||||
- Remove unused button classes `btn-create` and `comment-btn`. !23232 (George Tsiolis)
|
||||
- [API] Omit `X-Total` and `X-Total-Pages` headers when items count is more than 10,000. !23931
|
||||
- Improve efficiency of GitHub importer by reducing amount of locks needed. !24102
|
||||
- Improve milestone queries using subqueries instead of separate queries for ids. !24325
|
||||
- Efficiently remove expired artifacts in `ExpireBuildArtifactsWorker`. !24450
|
||||
- Eliminate N+1 queries in /api/groups/:id. !24513
|
||||
- Use deployment relation to get an environment name. !24890
|
||||
- Do not reload daemon if configuration file of pages does not change.
|
||||
|
||||
### Added (35 changes, 18 of them are from the community)
|
||||
|
||||
- Add badge count to projects. !18425 (George Tsiolis)
|
||||
- API: Add support for group labels. !21368 (Robert Schilling)
|
||||
- Add setting for first day of the week. !22755 (Fabian Schneider @fabsrc)
|
||||
- Pages for subgroups. !23505
|
||||
- Add support for customer provided encryption keys for Amazon S3 remote backups. !23797 (Pepijn Van Eeckhoudt)
|
||||
- Add Knative detailed view. !23863 (Chris Baumbauer)
|
||||
- Add group full path to project's shared_with_groups. !24052 (Mathieu Parent)
|
||||
- Added feature to specify a custom Auto DevOps chart repository. !24162 (walkafwalka)
|
||||
- Add flat-square badge style. !24172 (Fabian Schneider @fabsrc)
|
||||
- Display last activity and created at datetimes for users. !24181
|
||||
- Allow setting of feature gates per project. !24184
|
||||
- Save issues/merge request sorting options to backend. !24198
|
||||
- Added support for custom hosts/domains to Auto DevOps. !24248 (walkafwalka)
|
||||
- Adds milestone search. !24265 (Jacopo Beschi @jacopo-beschi)
|
||||
- Allow merge request diffs to be placed into an object store. !24276
|
||||
- Add Container Registry API with cleanup function. !24303
|
||||
- GitLab now supports the profile and email scopes from OpenID Connect. !24335 (Goten Xiao)
|
||||
- Add 'in' filter that modifies scope of 'search' filter to issues and merge requests API. !24350 (Hiroyuki Sato)
|
||||
- Add `with_programming_language` filter for projects to API. !24377 (Dylan MacKenzie)
|
||||
- API: Support searching for tags. !24385 (Robert Schilling)
|
||||
- Document graphicsmagick installation for source installation. !24404 (Alexis Reigel)
|
||||
- Redirect GET projects/:id to project page. !24467
|
||||
- Indicate on Issue Status if an Issue was Moved. !24470
|
||||
- Redeploy Auto DevOps deployment on variable updates. !24498 (walkafwalka)
|
||||
- Don't create new merge request pipeline without commits. !24503 (Hiroyuki Sato)
|
||||
- Add GitLab Pages predefined CI variables 'CI_PAGES_DOMAIN' and 'CI_PAGES_URL'. !24504 (Adrian Moisey)
|
||||
- Moves domain setting from Auto DevOps to Cluster's page. !24580
|
||||
- API allows setting the squash commit message when squashing a merge request. !24784
|
||||
- Added ability to upgrade cluster applications. !24789
|
||||
- Add argument iids for issues in GraphQL. !24802
|
||||
- Add repositories count to usage ping data. !24823
|
||||
- Add support for extensionless pages URLs. !24876
|
||||
- Add templates for most popular Pages templates. !24906
|
||||
- Introduce Internal API for searching environment names. !24923
|
||||
- Allow admins to invalidate markdown texts by setting local markdown version.
|
||||
|
||||
### Other (50 changes, 18 of them are from the community)
|
||||
|
||||
- Externalize strings from `/app/views/projects/project_members`. !23227 (Tao Wang)
|
||||
- Add CSS & JS global flags to represent browser and platform. !24017
|
||||
- Fix deprecation: Passing an argument to force an association to reload is now deprecated. !24136 (Jasper Maes)
|
||||
- Cleanup legacy artifact background migration. !24144
|
||||
- Bump kubectl in Auto DevOps to 1.11.6. !24176
|
||||
- Conditionally initialize the global opentracing tracer. !24186
|
||||
- Remove horizontal whitespace on user profile overview on small breakpoints. !24189
|
||||
- Bump nginx-ingress chart to 1.1.2. !24203
|
||||
- Use monospace font for registry table tag id and tag name. !24205
|
||||
- Rename project tags to project topics. !24219
|
||||
- Add uniqueness validation to url column in Releases::Link model. !24223
|
||||
- Update sidekiq-cron to 1.0.4 and use fugit to replace rufus-scheduler to parse cron syntax. !24235
|
||||
- Adds inter-service OpenTracing propagation. !24239
|
||||
- Fixes Auto DevOps title on CI/CD admin settings. !24249
|
||||
- Upgrade kubeclient to 4.2.2 and swap out monkey-patch to disallow redirects. !24284
|
||||
- i18n: externalize strings from 'app/views/search'. !24297 (Tao Wang)
|
||||
- Fix several ActionController::Parameters deprecations. !24332 (Jasper Maes)
|
||||
- Remove all `$theme-gray-{weight}` variables in favor of `$gray-{weight}`. !24333 (George Tsiolis)
|
||||
- Update gitlab-styles to 2.5.1. !24336 (Jasper Maes)
|
||||
- Modifies environment scope UI on cluster page. !24376
|
||||
- Extract process_name from GitLab::Sentry. !24422
|
||||
- Upgrade Gitaly to 1.13.0. !24429
|
||||
- Actually set raise_on_unfiltered_parameters to true. !24443 (Jasper Maes)
|
||||
- Refactored NoteableDiscussion by extracting ResolveDiscussionButton. !24505 (Martin Hobert)
|
||||
- Extracted JumpToNextDiscussionButton to its own component. !24506 (Martin Hobert)
|
||||
- Extracted ReplyPlaceholder to its own component. !24507 (Martin Hobert)
|
||||
- Block emojis and symbol characters from users full names. !24523
|
||||
- Update GitLab Runner Helm Chart to 0.1.45. !24564
|
||||
- Updated docs for fields in pushing mirror from GitLab to GitHub. !24566 (Joseph Yu)
|
||||
- Upgrade gitlab-workhorse to 8.1.0. !24571
|
||||
- Externalize strings from `/app/views/sent_notifications`. !24576 (George Tsiolis)
|
||||
- Adds tracing support for ActiveRecord notifications. !24604
|
||||
- Externalize strings from `/app/views/projects/ci`. !24617 (George Tsiolis)
|
||||
- Move permission check of manual actions of deployments. !24660
|
||||
- Externalize strings from `/app/views/clusters`. !24666 (George Tsiolis)
|
||||
- Update UI for admin appearance settings. !24685
|
||||
- Externalize strings from `/app/views/projects/pages_domains`. !24723 (George Tsiolis)
|
||||
- Externalize strings from `/app/views/projects/milestones`. !24726 (George Tsiolis)
|
||||
- Add OpenTracing instrumentation for Action View Render events. !24728
|
||||
- Expose version for each application in cluster_status JSON endpoint. !24791
|
||||
- Externalize strings from `/app/views/instance_statistics`. !24809 (George Tsiolis)
|
||||
- Update cluster application version on updated and installed status. !24810
|
||||
- Project list UI improvements. !24855
|
||||
- Externalize strings from `/app/views/email_rejection_mailer`. !24869 (George Tsiolis)
|
||||
- Update Gitaly to v1.17.0. !24873
|
||||
- Update Workhorse to v8.3.0. !24959
|
||||
- Upgrade gitaly to 1.18.0. !24981
|
||||
- Update Workhorse to v8.3.1.
|
||||
- Upgraded Codesandbox smooshpack package.
|
||||
- Creates mixin to reduce code duplication between CE and EE in graph component.
|
||||
|
||||
|
||||
## 11.7.5 (2019-02-06)
|
||||
|
||||
### Fixed (8 changes)
|
||||
|
@ -16,19 +263,15 @@ entry.
|
|||
- Changed external wiki query method to prevent attribute caching. !24907
|
||||
|
||||
|
||||
## 11.7.4 (2019-02-04)
|
||||
|
||||
### Security (1 change)
|
||||
|
||||
- Use sanitized user status message for user popover.
|
||||
|
||||
|
||||
## 11.7.3 (2019-01-30)
|
||||
|
||||
- No changes.
|
||||
|
||||
## 11.7.2 (2019-01-29)
|
||||
|
||||
### Fixed (1 change)
|
||||
|
||||
- Fix uninitialized constant with GitLab Pages.
|
||||
|
||||
|
||||
## 11.7.1 (2019-01-28)
|
||||
|
||||
### Security (24 changes)
|
||||
|
||||
- Make potentially malicious links more visible in the UI and scrub RTLO chars from links. !2770
|
||||
|
@ -56,14 +299,6 @@ entry.
|
|||
- Notify only users who can access the project on project move.
|
||||
- Alias GitHub and BitBucket OAuth2 callback URLs.
|
||||
|
||||
### Fixed (1 change)
|
||||
|
||||
- Fix uninitialized constant with GitLab Pages.
|
||||
|
||||
|
||||
## 11.7.1 (2019-01-28)
|
||||
|
||||
- Unreleased due to quality assurance failure.
|
||||
|
||||
## 11.7.0 (2019-01-22)
|
||||
|
||||
|
@ -251,6 +486,28 @@ entry.
|
|||
- Update url placeholder for the sentry configuration page. !24338
|
||||
|
||||
|
||||
## 11.6.8 (2019-01-30)
|
||||
|
||||
- No changes.
|
||||
|
||||
## 11.6.5 (2019-01-17)
|
||||
|
||||
### Fixed (5 changes)
|
||||
|
||||
- Add syntax highlighting to suggestion diff. !24156
|
||||
- Fix broken templated "Too many changes to show" text. !24282
|
||||
- Fix requests profiler in admin page not rendering HTML properly. !24291
|
||||
- Fix no avatar not showing in user selection box. !24346
|
||||
- Fixed diff suggestions removing dashes.
|
||||
|
||||
|
||||
## 11.6.4 (2019-01-15)
|
||||
|
||||
### Security (1 change)
|
||||
|
||||
- Validate bundle files before unpacking them.
|
||||
|
||||
|
||||
## 11.6.3 (2019-01-04)
|
||||
|
||||
### Fixed (1 change)
|
||||
|
@ -573,6 +830,33 @@ entry.
|
|||
- Enable Rubocop on lib/gitlab. (gfyoung)
|
||||
|
||||
|
||||
## 11.5.8 (2019-01-28)
|
||||
|
||||
### Security (21 changes)
|
||||
|
||||
- Make potentially malicious links more visible in the UI and scrub RTLO chars from links. !2770
|
||||
- Don't process MR refs for guests in the notes. !2771
|
||||
- Fixed XSS content in KaTex links.
|
||||
- Verify that LFS upload requests are genuine.
|
||||
- Extract GitLab Pages using RubyZip.
|
||||
- Prevent awarding emojis to notes whose parent is not visible to user.
|
||||
- Prevent unauthorized replies when discussion is locked or confidential.
|
||||
- Disable git v2 protocol temporarily.
|
||||
- Fix showing ci status for guest users when public pipline are not set.
|
||||
- Fix contributed projects info still visible when user enable private profile.
|
||||
- Disallows unauthorized users from accessing the pipelines section.
|
||||
- Add more LFS validations to prevent forgery.
|
||||
- Use common error for unauthenticated users when creating issues.
|
||||
- Fix slow regex in project reference pattern.
|
||||
- Fix private user email being visible in push (and tag push) webhooks.
|
||||
- Fix wiki access rights when external wiki is enabled.
|
||||
- Fix path disclosure on project import error.
|
||||
- Restrict project import visibility based on its group.
|
||||
- Expose CI/CD trigger token only to the trigger owner.
|
||||
- Notify only users who can access the project on project move.
|
||||
- Alias GitHub and BitBucket OAuth2 callback URLs.
|
||||
|
||||
|
||||
## 11.5.5 (2018-12-20)
|
||||
|
||||
### Security (1 change)
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.12.2
|
||||
1.20.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.3.1
|
||||
1.5.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
8.0.2
|
||||
8.3.1
|
||||
|
|
25
Gemfile
25
Gemfile
|
@ -16,7 +16,7 @@ gem 'gitlab-default_value_for', '~> 3.1.1', require: 'default_value_for'
|
|||
|
||||
# Supported DBs
|
||||
gem 'mysql2', '~> 0.4.10', group: :mysql
|
||||
gem 'pg', '~> 0.18.2', group: :postgres
|
||||
gem 'pg', '~> 1.1', group: :postgres
|
||||
|
||||
gem 'rugged', '~> 0.27'
|
||||
gem 'grape-path-helpers', '~> 1.0'
|
||||
|
@ -113,10 +113,9 @@ gem 'seed-fu', '~> 2.3.7'
|
|||
|
||||
# Markdown and HTML processing
|
||||
gem 'html-pipeline', '~> 2.8'
|
||||
gem 'deckar01-task_list', '2.0.0'
|
||||
gem 'deckar01-task_list', '2.2.0'
|
||||
gem 'gitlab-markup', '~> 1.6.5'
|
||||
gem 'github-markup', '~> 1.7.0', require: 'github/markup'
|
||||
gem 'redcarpet', '~> 3.4'
|
||||
gem 'commonmarker', '~> 0.17'
|
||||
gem 'RedCloth', '~> 4.3.2'
|
||||
gem 'rdoc', '~> 6.0'
|
||||
|
@ -126,9 +125,9 @@ gem 'wikicloth', '0.8.1'
|
|||
gem 'asciidoctor', '~> 1.5.8'
|
||||
gem 'asciidoctor-plantuml', '0.0.8'
|
||||
gem 'rouge', '~> 3.1'
|
||||
gem 'truncato', '~> 0.7.9'
|
||||
gem 'truncato', '~> 0.7.11'
|
||||
gem 'bootstrap_form', '~> 2.7.0'
|
||||
gem 'nokogiri', '~> 1.8.5'
|
||||
gem 'nokogiri', '~> 1.10.1'
|
||||
gem 'escape_utils', '~> 1.1'
|
||||
|
||||
# Calendar rendering
|
||||
|
@ -161,12 +160,12 @@ gem 'acts-as-taggable-on', '~> 5.0'
|
|||
|
||||
# Background jobs
|
||||
gem 'sidekiq', '~> 5.2.1'
|
||||
gem 'sidekiq-cron', '~> 0.6.0'
|
||||
gem 'sidekiq-cron', '~> 1.0'
|
||||
gem 'redis-namespace', '~> 1.6.0'
|
||||
gem 'gitlab-sidekiq-fetcher', '~> 0.4.0', require: 'sidekiq-reliable-fetch'
|
||||
|
||||
# Cron Parser
|
||||
gem 'rufus-scheduler', '~> 3.4'
|
||||
gem 'fugit', '~> 1.1'
|
||||
|
||||
# HTTP requests
|
||||
gem 'httparty', '~> 0.13.3'
|
||||
|
@ -188,7 +187,7 @@ gem 're2', '~> 1.1.1'
|
|||
gem 'version_sorter', '~> 2.1.0'
|
||||
|
||||
# Export Ruby Regex to Javascript
|
||||
gem 'js_regex', '~> 2.2.1'
|
||||
gem 'js_regex', '~> 3.1'
|
||||
|
||||
# User agent parsing
|
||||
gem 'device_detector'
|
||||
|
@ -225,7 +224,7 @@ gem 'asana', '~> 0.8.1'
|
|||
gem 'ruby-fogbugz', '~> 0.2.1'
|
||||
|
||||
# Kubernetes integration
|
||||
gem 'kubeclient', '~> 4.0.0'
|
||||
gem 'kubeclient', '~> 4.2.2'
|
||||
|
||||
# Sanitize user input
|
||||
gem 'sanitize', '~> 4.6'
|
||||
|
@ -305,6 +304,12 @@ group :metrics do
|
|||
gem 'raindrops', '~> 0.18'
|
||||
end
|
||||
|
||||
group :tracing do
|
||||
# OpenTracing
|
||||
gem 'opentracing', '~> 0.4.3'
|
||||
gem 'jaeger-client', '~> 0.10.0'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'foreman', '~> 0.84.0'
|
||||
gem 'brakeman', '~> 4.2', require: false
|
||||
|
@ -417,7 +422,7 @@ group :ed25519 do
|
|||
end
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly-proto', '~> 1.5.0', require: 'gitaly'
|
||||
gem 'gitaly-proto', '~> 1.10.0', require: 'gitaly'
|
||||
gem 'grpc', '~> 1.15.0'
|
||||
|
||||
gem 'google-protobuf', '~> 3.6'
|
||||
|
|
84
Gemfile.lock
84
Gemfile.lock
|
@ -113,6 +113,7 @@ GEM
|
|||
activesupport (>= 4.0.0)
|
||||
mime-types (>= 1.16)
|
||||
cause (0.1)
|
||||
character_set (1.1.2)
|
||||
charlock_holmes (0.7.6)
|
||||
childprocess (0.9.0)
|
||||
ffi (~> 1.0, >= 1.0.11)
|
||||
|
@ -143,7 +144,7 @@ GEM
|
|||
database_cleaner (1.7.0)
|
||||
debug_inspector (0.0.3)
|
||||
debugger-ruby_core_source (1.3.8)
|
||||
deckar01-task_list (2.0.0)
|
||||
deckar01-task_list (2.2.0)
|
||||
html-pipeline
|
||||
declarative (0.0.10)
|
||||
declarative-option (0.1.0)
|
||||
|
@ -185,7 +186,7 @@ GEM
|
|||
erubi (1.7.1)
|
||||
erubis (2.7.0)
|
||||
escape_utils (1.2.1)
|
||||
et-orbi (1.0.3)
|
||||
et-orbi (1.1.7)
|
||||
tzinfo
|
||||
eventmachine (1.2.7)
|
||||
excon (0.62.0)
|
||||
|
@ -206,7 +207,7 @@ GEM
|
|||
fast_blank (1.0.0)
|
||||
fast_gettext (1.6.0)
|
||||
ffaker (2.10.0)
|
||||
ffi (1.9.25)
|
||||
ffi (1.10.0)
|
||||
flipper (0.13.0)
|
||||
flipper-active_record (0.13.0)
|
||||
activerecord (>= 3.2, < 6)
|
||||
|
@ -258,6 +259,9 @@ GEM
|
|||
foreman (0.84.0)
|
||||
thor (~> 0.19.1)
|
||||
formatador (0.2.5)
|
||||
fugit (1.1.7)
|
||||
et-orbi (~> 1.1, >= 1.1.7)
|
||||
raabro (~> 1.1)
|
||||
fuubar (2.2.0)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
|
@ -274,7 +278,7 @@ GEM
|
|||
gettext_i18n_rails (>= 0.7.1)
|
||||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gitaly-proto (1.5.0)
|
||||
gitaly-proto (1.10.0)
|
||||
grpc (~> 1.0)
|
||||
github-markup (1.7.0)
|
||||
gitlab-default_value_for (3.1.1)
|
||||
|
@ -282,7 +286,7 @@ GEM
|
|||
gitlab-markup (1.6.5)
|
||||
gitlab-sidekiq-fetcher (0.4.0)
|
||||
sidekiq (~> 5)
|
||||
gitlab-styles (2.4.1)
|
||||
gitlab-styles (2.5.1)
|
||||
rubocop (~> 0.54.0)
|
||||
rubocop-gitlab-security (~> 0.1.0)
|
||||
rubocop-rspec (~> 1.19)
|
||||
|
@ -389,13 +393,18 @@ GEM
|
|||
cause
|
||||
json
|
||||
ipaddress (0.8.3)
|
||||
jaeger-client (0.10.0)
|
||||
opentracing (~> 0.3)
|
||||
thrift
|
||||
jira-ruby (1.4.1)
|
||||
activesupport
|
||||
multipart-post
|
||||
oauth (~> 0.5, >= 0.5.0)
|
||||
jquery-atwho-rails (1.3.2)
|
||||
js_regex (2.2.1)
|
||||
regexp_parser (>= 0.4.11, <= 0.5.0)
|
||||
js_regex (3.1.1)
|
||||
character_set (~> 1.1)
|
||||
regexp_parser (~> 1.1)
|
||||
regexp_property_values (~> 0.3)
|
||||
json (1.8.6)
|
||||
json-jwt (1.9.4)
|
||||
activesupport
|
||||
|
@ -419,7 +428,7 @@ GEM
|
|||
kgio (2.10.0)
|
||||
knapsack (1.17.0)
|
||||
rake
|
||||
kubeclient (4.0.0)
|
||||
kubeclient (4.2.2)
|
||||
http (~> 3.0)
|
||||
recursive-open-struct (~> 1.0, >= 1.0.4)
|
||||
rest-client (~> 2.0)
|
||||
|
@ -462,9 +471,9 @@ GEM
|
|||
mimemagic (0.3.2)
|
||||
mini_magick (4.8.0)
|
||||
mini_mime (1.0.1)
|
||||
mini_portile2 (2.3.0)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.11.3)
|
||||
msgpack (1.2.4)
|
||||
msgpack (1.2.6)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
|
@ -477,8 +486,8 @@ GEM
|
|||
net-ssh (5.0.1)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.3.1)
|
||||
nokogiri (1.8.5)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
nokogiri (1.10.1)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogumbo (1.5.0)
|
||||
nokogiri
|
||||
numerizer (0.1.1)
|
||||
|
@ -544,6 +553,8 @@ GEM
|
|||
activesupport
|
||||
nokogiri (>= 1.4.4)
|
||||
omniauth (~> 1.0)
|
||||
opentracing (0.4.3)
|
||||
optimist (3.0.0)
|
||||
org-ruby (0.9.12)
|
||||
rubypants (~> 0.2)
|
||||
orm_adapter (0.5.0)
|
||||
|
@ -575,7 +586,7 @@ GEM
|
|||
atomic (>= 1.0.0)
|
||||
peek
|
||||
redis
|
||||
pg (0.18.4)
|
||||
pg (1.1.3)
|
||||
po_to_json (1.0.1)
|
||||
json (>= 1.6.0)
|
||||
powerpack (0.1.1)
|
||||
|
@ -606,6 +617,7 @@ GEM
|
|||
get_process_mem (~> 0.2)
|
||||
puma (>= 2.7, < 4)
|
||||
pyu-ruby-sasl (0.0.3.3)
|
||||
raabro (1.1.6)
|
||||
rack (2.0.6)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
|
@ -618,7 +630,7 @@ GEM
|
|||
httpclient (>= 2.4)
|
||||
multi_json (>= 1.3.6)
|
||||
rack (>= 1.1)
|
||||
rack-protection (2.0.4)
|
||||
rack-protection (2.0.5)
|
||||
rack
|
||||
rack-proxy (0.6.0)
|
||||
rack
|
||||
|
@ -664,16 +676,15 @@ GEM
|
|||
ffi (>= 0.5.0, < 2)
|
||||
rblineprof (0.3.6)
|
||||
debugger-ruby_core_source (~> 1.3)
|
||||
rbtrace (0.4.10)
|
||||
rbtrace (0.4.11)
|
||||
ffi (>= 1.0.6)
|
||||
msgpack (>= 0.4.3)
|
||||
trollop (>= 1.16.2)
|
||||
optimist (>= 3.0.0)
|
||||
rdoc (6.0.4)
|
||||
re2 (1.1.1)
|
||||
recaptcha (3.0.0)
|
||||
json
|
||||
recursive-open-struct (1.1.0)
|
||||
redcarpet (3.4.0)
|
||||
redis (3.3.5)
|
||||
redis-actionpack (5.0.2)
|
||||
actionpack (>= 4.0, < 6)
|
||||
|
@ -693,7 +704,8 @@ GEM
|
|||
redis-store (>= 1.2, < 2)
|
||||
redis-store (1.6.0)
|
||||
redis (>= 2.2, < 5)
|
||||
regexp_parser (0.5.0)
|
||||
regexp_parser (1.3.0)
|
||||
regexp_property_values (0.3.4)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
|
@ -775,8 +787,6 @@ GEM
|
|||
rubyntlm (0.6.2)
|
||||
rubypants (0.2.0)
|
||||
rubyzip (1.2.2)
|
||||
rufus-scheduler (3.4.0)
|
||||
et-orbi (~> 1.0)
|
||||
rugged (0.27.5)
|
||||
safe_yaml (1.0.4)
|
||||
sanitize (4.6.6)
|
||||
|
@ -816,12 +826,13 @@ GEM
|
|||
rack
|
||||
shoulda-matchers (3.1.2)
|
||||
activesupport (>= 4.0.0)
|
||||
sidekiq (5.2.3)
|
||||
sidekiq (5.2.5)
|
||||
connection_pool (~> 2.2, >= 2.2.2)
|
||||
rack (>= 1.5.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (>= 3.3.5, < 5)
|
||||
sidekiq-cron (0.6.0)
|
||||
rufus-scheduler (>= 3.3.0)
|
||||
sidekiq-cron (1.0.4)
|
||||
fugit (~> 1.1)
|
||||
sidekiq (>= 4.2.1)
|
||||
signet (0.11.0)
|
||||
addressable (~> 2.3)
|
||||
|
@ -868,6 +879,7 @@ GEM
|
|||
rack (>= 1, < 3)
|
||||
thor (0.19.4)
|
||||
thread_safe (0.3.6)
|
||||
thrift (0.11.0.0)
|
||||
tilt (2.0.8)
|
||||
timecop (0.8.1)
|
||||
timfel-krb5-auth (0.8.3)
|
||||
|
@ -875,10 +887,9 @@ GEM
|
|||
parslet (~> 1.8.0)
|
||||
toml-rb (1.0.0)
|
||||
citrus (~> 3.0, > 3.0)
|
||||
trollop (2.1.3)
|
||||
truncato (0.7.10)
|
||||
truncato (0.7.11)
|
||||
htmlentities (~> 4.3.1)
|
||||
nokogiri (~> 1.8.0, >= 1.7.0)
|
||||
nokogiri (>= 1.7.0, <= 2.0)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
u2f (0.2.1)
|
||||
|
@ -974,7 +985,7 @@ DEPENDENCIES
|
|||
connection_pool (~> 2.0)
|
||||
creole (~> 0.5.0)
|
||||
database_cleaner (~> 1.7.0)
|
||||
deckar01-task_list (= 2.0.0)
|
||||
deckar01-task_list (= 2.2.0)
|
||||
device_detector
|
||||
devise (~> 4.4)
|
||||
devise-two-factor (~> 3.0.0)
|
||||
|
@ -1003,12 +1014,13 @@ DEPENDENCIES
|
|||
fog-rackspace (~> 0.1.1)
|
||||
font-awesome-rails (~> 4.7)
|
||||
foreman (~> 0.84.0)
|
||||
fugit (~> 1.1)
|
||||
fuubar (~> 2.2.0)
|
||||
gemojione (~> 3.3)
|
||||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 1.5.0)
|
||||
gitaly-proto (~> 1.10.0)
|
||||
github-markup (~> 1.7.0)
|
||||
gitlab-default_value_for (~> 3.1.1)
|
||||
gitlab-markup (~> 1.6.5)
|
||||
|
@ -1037,14 +1049,15 @@ DEPENDENCIES
|
|||
httparty (~> 0.13.3)
|
||||
icalendar
|
||||
influxdb (~> 0.2)
|
||||
jaeger-client (~> 0.10.0)
|
||||
jira-ruby (~> 1.4)
|
||||
jquery-atwho-rails (~> 1.3.2)
|
||||
js_regex (~> 2.2.1)
|
||||
js_regex (~> 3.1)
|
||||
json-schema (~> 2.8.0)
|
||||
jwt (~> 2.1.0)
|
||||
kaminari (~> 1.0)
|
||||
knapsack (~> 1.17)
|
||||
kubeclient (~> 4.0.0)
|
||||
kubeclient (~> 4.2.2)
|
||||
letter_opener_web (~> 1.3.0)
|
||||
license_finder (~> 5.4)
|
||||
licensee (~> 8.9)
|
||||
|
@ -1059,7 +1072,7 @@ DEPENDENCIES
|
|||
nakayoshi_fork (~> 0.0.4)
|
||||
net-ldap
|
||||
net-ssh (~> 5.0)
|
||||
nokogiri (~> 1.8.5)
|
||||
nokogiri (~> 1.10.1)
|
||||
oauth2 (~> 1.4)
|
||||
octokit (~> 4.9)
|
||||
omniauth (~> 1.8)
|
||||
|
@ -1077,6 +1090,7 @@ DEPENDENCIES
|
|||
omniauth-shibboleth (~> 1.3.0)
|
||||
omniauth-twitter (~> 1.4)
|
||||
omniauth_crowd (~> 2.2.0)
|
||||
opentracing (~> 0.4.3)
|
||||
org-ruby (~> 0.9.12)
|
||||
peek (~> 1.0.1)
|
||||
peek-gc (~> 0.0.2)
|
||||
|
@ -1084,7 +1098,7 @@ DEPENDENCIES
|
|||
peek-pg (~> 1.3.0)
|
||||
peek-rblineprof (~> 0.2.0)
|
||||
peek-redis (~> 1.2.0)
|
||||
pg (~> 0.18.2)
|
||||
pg (~> 1.1)
|
||||
premailer-rails (~> 1.9.7)
|
||||
prometheus-client-mmap (~> 0.9.4)
|
||||
pry-byebug (~> 3.5.1)
|
||||
|
@ -1107,7 +1121,6 @@ DEPENDENCIES
|
|||
rdoc (~> 6.0)
|
||||
re2 (~> 1.1.1)
|
||||
recaptcha (~> 3.0)
|
||||
redcarpet (~> 3.4)
|
||||
redis (~> 3.2)
|
||||
redis-namespace (~> 1.6.0)
|
||||
redis-rails (~> 5.0.2)
|
||||
|
@ -1128,7 +1141,6 @@ DEPENDENCIES
|
|||
ruby-progressbar
|
||||
ruby_parser (~> 3.8)
|
||||
rubyzip (~> 1.2.2)
|
||||
rufus-scheduler (~> 3.4)
|
||||
rugged (~> 0.27)
|
||||
sanitize (~> 4.6)
|
||||
sass (~> 3.5)
|
||||
|
@ -1142,7 +1154,7 @@ DEPENDENCIES
|
|||
sham_rack (~> 1.3.6)
|
||||
shoulda-matchers (~> 3.1.2)
|
||||
sidekiq (~> 5.2.1)
|
||||
sidekiq-cron (~> 0.6.0)
|
||||
sidekiq-cron (~> 1.0)
|
||||
simple_po_parser (~> 1.1.2)
|
||||
simplecov (~> 0.14.0)
|
||||
slack-notifier (~> 1.5.1)
|
||||
|
@ -1157,7 +1169,7 @@ DEPENDENCIES
|
|||
thin (~> 1.7.0)
|
||||
timecop (~> 0.8.0)
|
||||
toml-rb (~> 1.0.0)
|
||||
truncato (~> 0.7.9)
|
||||
truncato (~> 0.7.11)
|
||||
u2f (~> 0.2.1)
|
||||
uglifier (~> 2.7.2)
|
||||
unf (~> 0.1.4)
|
||||
|
|
|
@ -56,7 +56,7 @@ Below we describe the contributing process to GitLab for two reasons:
|
|||
Several people from the [GitLab team][team] are helping community members to get
|
||||
their contributions accepted by meeting our [Definition of done][done].
|
||||
|
||||
What you can expect from them is described at https://about.gitlab.com/roles/merge-request-coach/.
|
||||
What you can expect from them is described at https://about.gitlab.com/job-families/expert/merge-request-coach/.
|
||||
|
||||
### Milestones on community contribution issues
|
||||
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
11.7.5
|
||||
11.8.0
|
||||
|
|
|
@ -5,6 +5,7 @@ import axios from './lib/utils/axios_utils';
|
|||
const Api = {
|
||||
groupsPath: '/api/:version/groups.json',
|
||||
groupPath: '/api/:version/groups/:id',
|
||||
groupMembersPath: '/api/:version/groups/:id/members',
|
||||
subgroupsPath: '/api/:version/groups/:id/subgroups',
|
||||
namespacesPath: '/api/:version/namespaces.json',
|
||||
groupProjectsPath: '/api/:version/groups/:id/projects.json',
|
||||
|
@ -40,6 +41,12 @@ const Api = {
|
|||
});
|
||||
},
|
||||
|
||||
groupMembers(id) {
|
||||
const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id));
|
||||
|
||||
return axios.get(url);
|
||||
},
|
||||
|
||||
// Return groups list. Filtered by query
|
||||
groups(query, options, callback = $.noop) {
|
||||
const url = Api.buildUrl(Api.groupsPath);
|
||||
|
|
|
@ -437,7 +437,7 @@ export class AwardsHandler {
|
|||
|
||||
createAwardButtonForVotesBlock(votesBlock, emojiName) {
|
||||
const buttonHtml = `
|
||||
<button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
|
||||
<button class="btn award-control js-emoji-btn has-tooltip active" title="You">
|
||||
${this.emoji.glEmojiTag(emojiName)}
|
||||
<span class="award-control-text js-counter">1</span>
|
||||
</button>
|
||||
|
|
|
@ -55,7 +55,7 @@ export default {
|
|||
:disabled="badge.isDeleting"
|
||||
class="btn btn-default append-right-8"
|
||||
type="button"
|
||||
@click="editBadge(badge);"
|
||||
@click="editBadge(badge)"
|
||||
>
|
||||
<icon :size="16" :aria-label="__('Edit')" name="pencil" />
|
||||
</button>
|
||||
|
@ -65,7 +65,7 @@ export default {
|
|||
type="button"
|
||||
data-toggle="modal"
|
||||
data-target="#delete-badge-modal"
|
||||
@click="updateBadgeInModal(badge);"
|
||||
@click="updateBadgeInModal(badge)"
|
||||
>
|
||||
<icon :size="16" :aria-label="__('Delete')" name="remove" />
|
||||
</button>
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import installCustomElements from 'document-register-element';
|
||||
import 'document-register-element';
|
||||
import isEmojiUnicodeSupported from '../emoji/support';
|
||||
|
||||
installCustomElements(window);
|
||||
class GlEmoji extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
export default function installGlEmojiElement() {
|
||||
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
|
||||
GlEmojiElementProto.createdCallback = function createdCallback() {
|
||||
const emojiUnicode = this.textContent.trim();
|
||||
const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
|
||||
|
||||
|
@ -43,9 +42,11 @@ export default function installGlEmojiElement() {
|
|||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.registerElement('gl-emoji', {
|
||||
prototype: GlEmojiElementProto,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function installGlEmojiElement() {
|
||||
if (!customElements.get('gl-emoji')) {
|
||||
customElements.define('gl-emoji', GlEmoji);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,320 +1,5 @@
|
|||
/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, no-restricted-syntax, guard-for-in, no-continue */
|
||||
|
||||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
|
||||
const gfmRules = {
|
||||
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
|
||||
// GitLab Flavored Markdown (GFM) to HTML.
|
||||
// These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
|
||||
// Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
|
||||
// from GFM should have a handler here, in reverse order.
|
||||
// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
|
||||
InlineDiffFilter: {
|
||||
'span.idiff.addition'(el, text) {
|
||||
return `{+${text}+}`;
|
||||
},
|
||||
'span.idiff.deletion'(el, text) {
|
||||
return `{-${text}-}`;
|
||||
},
|
||||
},
|
||||
TaskListFilter: {
|
||||
'input[type=checkbox].task-list-item-checkbox'(el) {
|
||||
return `[${el.checked ? 'x' : ' '}]`;
|
||||
},
|
||||
},
|
||||
ReferenceFilter: {
|
||||
'.tooltip'(el) {
|
||||
return '';
|
||||
},
|
||||
'a.gfm:not([data-link=true])'(el, text) {
|
||||
return el.dataset.original || text;
|
||||
},
|
||||
},
|
||||
AutolinkFilter: {
|
||||
a(el, text) {
|
||||
// Fallback on the regular MarkdownFilter's `a` handler.
|
||||
if (text !== el.getAttribute('href')) return false;
|
||||
|
||||
return text;
|
||||
},
|
||||
},
|
||||
TableOfContentsFilter: {
|
||||
'ul.section-nav'(el) {
|
||||
return '[[_TOC_]]';
|
||||
},
|
||||
},
|
||||
EmojiFilter: {
|
||||
'img.emoji'(el) {
|
||||
return el.getAttribute('alt');
|
||||
},
|
||||
'gl-emoji'(el) {
|
||||
return `:${el.getAttribute('data-name')}:`;
|
||||
},
|
||||
},
|
||||
ImageLinkFilter: {
|
||||
'a.no-attachment-icon'(el, text) {
|
||||
return text;
|
||||
},
|
||||
},
|
||||
ImageLazyLoadFilter: {
|
||||
img(el, text) {
|
||||
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
|
||||
},
|
||||
},
|
||||
VideoLinkFilter: {
|
||||
'.video-container'(el) {
|
||||
const videoEl = el.querySelector('video');
|
||||
if (!videoEl) return false;
|
||||
|
||||
return CopyAsGFM.nodeToGFM(videoEl);
|
||||
},
|
||||
video(el) {
|
||||
return `![${el.dataset.title}](${el.getAttribute('src')})`;
|
||||
},
|
||||
},
|
||||
MermaidFilter: {
|
||||
'svg.mermaid'(el, text) {
|
||||
const sourceEl = el.querySelector('text.source');
|
||||
if (!sourceEl) return false;
|
||||
|
||||
return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``;
|
||||
},
|
||||
'svg.mermaid style, svg.mermaid g'(el, text) {
|
||||
// We don't want to include the content of these elements in the copied text.
|
||||
return '';
|
||||
},
|
||||
},
|
||||
MathFilter: {
|
||||
'pre.code.math[data-math-style=display]'(el, text) {
|
||||
return `\`\`\`math\n${text.trim()}\n\`\`\``;
|
||||
},
|
||||
'code.code.math[data-math-style=inline]'(el, text) {
|
||||
return `$\`${text}\`$`;
|
||||
},
|
||||
'span.katex-display span.katex-mathml'(el) {
|
||||
const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
|
||||
if (!mathAnnotation) return false;
|
||||
|
||||
return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
|
||||
},
|
||||
'span.katex-mathml'(el) {
|
||||
const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
|
||||
if (!mathAnnotation) return false;
|
||||
|
||||
return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
|
||||
},
|
||||
'span.katex-html'(el) {
|
||||
// We don't want to include the content of this element in the copied text.
|
||||
return '';
|
||||
},
|
||||
'annotation[encoding="application/x-tex"]'(el, text) {
|
||||
return text.trim();
|
||||
},
|
||||
},
|
||||
SanitizationFilter: {
|
||||
'a[name]:not([href]):empty'(el) {
|
||||
return el.outerHTML;
|
||||
},
|
||||
dl(el, text) {
|
||||
let lines = text
|
||||
.replace(/\n\n/g, '\n')
|
||||
.trim()
|
||||
.split('\n');
|
||||
// Add two spaces to the front of subsequent list items lines,
|
||||
// or leave the line entirely blank.
|
||||
lines = lines.map(l => {
|
||||
const line = l.trim();
|
||||
if (line.length === 0) return '';
|
||||
|
||||
return ` ${line}`;
|
||||
});
|
||||
|
||||
return `<dl>\n${lines.join('\n')}\n</dl>\n`;
|
||||
},
|
||||
'dt, dd, summary, details'(el, text) {
|
||||
const tag = el.nodeName.toLowerCase();
|
||||
return `<${tag}>${text}</${tag}>\n`;
|
||||
},
|
||||
'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
|
||||
const tag = el.nodeName.toLowerCase();
|
||||
return `<${tag}>${text}</${tag}>`;
|
||||
},
|
||||
},
|
||||
SyntaxHighlightFilter: {
|
||||
'pre.code.highlight'(el, t) {
|
||||
const text = t.trimRight();
|
||||
|
||||
let lang = el.getAttribute('lang');
|
||||
if (!lang || lang === 'plaintext') {
|
||||
lang = '';
|
||||
}
|
||||
|
||||
// Prefixes lines with 4 spaces if the code contains triple backticks
|
||||
if (lang === '' && text.match(/^```/gm)) {
|
||||
return text
|
||||
.split('\n')
|
||||
.map(l => {
|
||||
const line = l.trim();
|
||||
if (line.length === 0) return '';
|
||||
|
||||
return ` ${line}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return `\`\`\`${lang}\n${text}\n\`\`\``;
|
||||
},
|
||||
'pre > code'(el, text) {
|
||||
// Don't wrap code blocks in ``
|
||||
return text;
|
||||
},
|
||||
},
|
||||
MarkdownFilter: {
|
||||
br(el) {
|
||||
// Two spaces at the end of a line are turned into a BR
|
||||
return ' ';
|
||||
},
|
||||
code(el, text) {
|
||||
let backtickCount = 1;
|
||||
const backtickMatch = text.match(/`+/);
|
||||
if (backtickMatch) {
|
||||
backtickCount = backtickMatch[0].length + 1;
|
||||
}
|
||||
|
||||
const backticks = Array(backtickCount + 1).join('`');
|
||||
const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
|
||||
|
||||
return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
|
||||
},
|
||||
blockquote(el, text) {
|
||||
return text
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(s => `> ${s}`.trim())
|
||||
.join('\n');
|
||||
},
|
||||
img(el) {
|
||||
const imageSrc = el.src;
|
||||
const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
|
||||
return `![${el.getAttribute('alt')}](${imageUrl})`;
|
||||
},
|
||||
'a.anchor'(el, text) {
|
||||
// Don't render a Markdown link for the anchor link inside a heading
|
||||
return text;
|
||||
},
|
||||
a(el, text) {
|
||||
return `[${text}](${el.getAttribute('href')})`;
|
||||
},
|
||||
li(el, text) {
|
||||
const lines = text.trim().split('\n');
|
||||
const firstLine = `- ${lines.shift()}`;
|
||||
// Add four spaces to the front of subsequent list items lines,
|
||||
// or leave the line entirely blank.
|
||||
const nextLines = lines.map(s => {
|
||||
if (s.trim().length === 0) return '';
|
||||
|
||||
return ` ${s}`;
|
||||
});
|
||||
|
||||
return `${firstLine}\n${nextLines.join('\n')}`;
|
||||
},
|
||||
ul(el, text) {
|
||||
return text;
|
||||
},
|
||||
ol(el, text) {
|
||||
// LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
|
||||
return text.replace(/^- /gm, '1. ');
|
||||
},
|
||||
h1(el, text) {
|
||||
return `# ${text.trim()}\n`;
|
||||
},
|
||||
h2(el, text) {
|
||||
return `## ${text.trim()}\n`;
|
||||
},
|
||||
h3(el, text) {
|
||||
return `### ${text.trim()}\n`;
|
||||
},
|
||||
h4(el, text) {
|
||||
return `#### ${text.trim()}\n`;
|
||||
},
|
||||
h5(el, text) {
|
||||
return `##### ${text.trim()}\n`;
|
||||
},
|
||||
h6(el, text) {
|
||||
return `###### ${text.trim()}\n`;
|
||||
},
|
||||
strong(el, text) {
|
||||
return `**${text}**`;
|
||||
},
|
||||
em(el, text) {
|
||||
return `_${text}_`;
|
||||
},
|
||||
del(el, text) {
|
||||
return `~~${text}~~`;
|
||||
},
|
||||
hr(el) {
|
||||
// extra leading \n is to ensure that there is a blank line between
|
||||
// a list followed by an hr, otherwise this breaks old redcarpet rendering
|
||||
return '\n-----\n';
|
||||
},
|
||||
p(el, text) {
|
||||
return `${text.trim()}\n`;
|
||||
},
|
||||
table(el) {
|
||||
const theadEl = el.querySelector('thead');
|
||||
const tbodyEl = el.querySelector('tbody');
|
||||
if (!theadEl || !tbodyEl) return false;
|
||||
|
||||
const theadText = CopyAsGFM.nodeToGFM(theadEl);
|
||||
const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
|
||||
|
||||
return [theadText, tbodyText].join('\n');
|
||||
},
|
||||
thead(el, text) {
|
||||
const cells = _.map(el.querySelectorAll('th'), cell => {
|
||||
let chars = CopyAsGFM.nodeToGFM(cell).length + 2;
|
||||
|
||||
let before = '';
|
||||
let after = '';
|
||||
const alignment = cell.align || cell.style.textAlign;
|
||||
|
||||
switch (alignment) {
|
||||
case 'center':
|
||||
before = ':';
|
||||
after = ':';
|
||||
chars -= 2;
|
||||
break;
|
||||
case 'right':
|
||||
after = ':';
|
||||
chars -= 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
chars = Math.max(chars, 3);
|
||||
|
||||
const middle = Array(chars + 1).join('-');
|
||||
|
||||
return before + middle + after;
|
||||
});
|
||||
|
||||
const separatorRow = `|${cells.join('|')}|`;
|
||||
|
||||
return [text, separatorRow].join('\n');
|
||||
},
|
||||
tr(el) {
|
||||
const cellEls = el.querySelectorAll('td, th');
|
||||
if (cellEls.length === 0) return false;
|
||||
|
||||
const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell));
|
||||
return `| ${cells.join(' | ')} |`;
|
||||
},
|
||||
},
|
||||
};
|
||||
import { getSelectedFragment } from '~/lib/utils/common_utils';
|
||||
|
||||
export class CopyAsGFM {
|
||||
constructor() {
|
||||
|
@ -347,8 +32,24 @@ export class CopyAsGFM {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(el.cloneNode(true));
|
||||
const html = div.innerHTML;
|
||||
|
||||
clipboardData.setData('text/plain', el.textContent);
|
||||
clipboardData.setData('text/x-gfm', this.nodeToGFM(el));
|
||||
clipboardData.setData('text/html', html);
|
||||
// We are also setting this as fallback to transform the selection to gfm on paste
|
||||
clipboardData.setData('text/x-gfm-html', html);
|
||||
|
||||
CopyAsGFM.nodeToGFM(el)
|
||||
.then(res => {
|
||||
clipboardData.setData('text/x-gfm', res);
|
||||
})
|
||||
.catch(() => {
|
||||
// Not showing the error as Firefox might doesn't allow
|
||||
// it or other browsers who have a time limit on the execution
|
||||
// of the copy event
|
||||
});
|
||||
}
|
||||
|
||||
static pasteGFM(e) {
|
||||
|
@ -357,11 +58,28 @@ export class CopyAsGFM {
|
|||
|
||||
const text = clipboardData.getData('text/plain');
|
||||
const gfm = clipboardData.getData('text/x-gfm');
|
||||
if (!gfm) return;
|
||||
const gfmHtml = clipboardData.getData('text/x-gfm-html');
|
||||
if (!gfm && !gfmHtml) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
window.gl.utils.insertText(e.target, (textBefore, textAfter) => {
|
||||
// We have the original selection already converted to gfm
|
||||
if (gfm) {
|
||||
CopyAsGFM.insertPastedText(e.target, text, gfm);
|
||||
} else {
|
||||
// Due to the async copy call we are not able to produce gfm so we transform the cached HTML
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = gfmHtml;
|
||||
CopyAsGFM.nodeToGFM(div)
|
||||
.then(transformedGfm => {
|
||||
CopyAsGFM.insertPastedText(e.target, text, transformedGfm);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
static insertPastedText(target, text, gfm) {
|
||||
window.gl.utils.insertText(target, textBefore => {
|
||||
// If the text before the cursor contains an odd number of backticks,
|
||||
// we are either inside an inline code span that starts with 1 backtick
|
||||
// or a code block that starts with 3 backticks.
|
||||
|
@ -443,75 +161,22 @@ export class CopyAsGFM {
|
|||
return codeElement;
|
||||
}
|
||||
|
||||
static nodeToGFM(node, respectWhitespaceParam = false) {
|
||||
if (node.nodeType === Node.COMMENT_NODE) {
|
||||
return '';
|
||||
}
|
||||
static nodeToGFM(node) {
|
||||
return Promise.all([
|
||||
import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'),
|
||||
import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'),
|
||||
import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'),
|
||||
])
|
||||
.then(([prosemirrorModel, schema, markdownSerializer]) => {
|
||||
const { DOMParser } = prosemirrorModel;
|
||||
const wrapEl = document.createElement('div');
|
||||
wrapEl.appendChild(node.cloneNode(true));
|
||||
const doc = DOMParser.fromSchema(schema.default).parse(wrapEl);
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return node.textContent;
|
||||
}
|
||||
|
||||
const respectWhitespace =
|
||||
respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE');
|
||||
|
||||
const text = this.innerGFM(node, respectWhitespace);
|
||||
|
||||
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
||||
return text;
|
||||
}
|
||||
|
||||
for (const filter in gfmRules) {
|
||||
const rules = gfmRules[filter];
|
||||
|
||||
for (const selector in rules) {
|
||||
const func = rules[selector];
|
||||
|
||||
if (!nodeMatchesSelector(node, selector)) continue;
|
||||
|
||||
let result;
|
||||
if (func.length === 2) {
|
||||
// if `func` takes 2 arguments, it depends on text.
|
||||
// if there is no text, we don't need to generate GFM for this node.
|
||||
if (text.length === 0) continue;
|
||||
|
||||
result = func(node, text);
|
||||
} else {
|
||||
result = func(node);
|
||||
}
|
||||
|
||||
if (result === false) continue;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
static innerGFM(parentNode, respectWhitespace = false) {
|
||||
const nodes = parentNode.childNodes;
|
||||
|
||||
const clonedParentNode = parentNode.cloneNode(true);
|
||||
const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
|
||||
|
||||
for (let i = 0; i < nodes.length; i += 1) {
|
||||
const node = nodes[i];
|
||||
const clonedNode = clonedNodes[i];
|
||||
|
||||
const text = this.nodeToGFM(node, respectWhitespace);
|
||||
|
||||
// `clonedNode.replaceWith(text)` is not yet widely supported
|
||||
clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
|
||||
}
|
||||
|
||||
let nodeText = clonedParentNode.innerText || clonedParentNode.textContent;
|
||||
|
||||
if (!respectWhitespace) {
|
||||
nodeText = nodeText.trim();
|
||||
}
|
||||
|
||||
return nodeText;
|
||||
const res = markdownSerializer.default.serialize(doc);
|
||||
return res;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
106
app/assets/javascripts/behaviors/markdown/editor_extensions.js
Normal file
106
app/assets/javascripts/behaviors/markdown/editor_extensions.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
import Doc from './nodes/doc';
|
||||
import Paragraph from './nodes/paragraph';
|
||||
import Text from './nodes/text';
|
||||
|
||||
import Blockquote from './nodes/blockquote';
|
||||
import CodeBlock from './nodes/code_block';
|
||||
import HardBreak from './nodes/hard_break';
|
||||
import Heading from './nodes/heading';
|
||||
import HorizontalRule from './nodes/horizontal_rule';
|
||||
import Image from './nodes/image';
|
||||
|
||||
import Table from './nodes/table';
|
||||
import TableHead from './nodes/table_head';
|
||||
import TableBody from './nodes/table_body';
|
||||
import TableHeaderRow from './nodes/table_header_row';
|
||||
import TableRow from './nodes/table_row';
|
||||
import TableCell from './nodes/table_cell';
|
||||
|
||||
import Emoji from './nodes/emoji';
|
||||
import Reference from './nodes/reference';
|
||||
|
||||
import TableOfContents from './nodes/table_of_contents';
|
||||
import Video from './nodes/video';
|
||||
|
||||
import BulletList from './nodes/bullet_list';
|
||||
import OrderedList from './nodes/ordered_list';
|
||||
import ListItem from './nodes/list_item';
|
||||
|
||||
import DescriptionList from './nodes/description_list';
|
||||
import DescriptionTerm from './nodes/description_term';
|
||||
import DescriptionDetails from './nodes/description_details';
|
||||
|
||||
import TaskList from './nodes/task_list';
|
||||
import OrderedTaskList from './nodes/ordered_task_list';
|
||||
import TaskListItem from './nodes/task_list_item';
|
||||
|
||||
import Summary from './nodes/summary';
|
||||
import Details from './nodes/details';
|
||||
|
||||
import Bold from './marks/bold';
|
||||
import Italic from './marks/italic';
|
||||
import Strike from './marks/strike';
|
||||
import InlineDiff from './marks/inline_diff';
|
||||
|
||||
import Link from './marks/link';
|
||||
import Code from './marks/code';
|
||||
import MathMark from './marks/math';
|
||||
import InlineHTML from './marks/inline_html';
|
||||
|
||||
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform
|
||||
// GitLab Flavored Markdown (GFM) to HTML.
|
||||
// The nodes and marks referenced here transform that same HTML to GFM to be copied to the clipboard.
|
||||
// Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
|
||||
// from GFM should have a node or mark here.
|
||||
// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
|
||||
|
||||
export default [
|
||||
new Doc(),
|
||||
new Paragraph(),
|
||||
new Text(),
|
||||
|
||||
new Blockquote(),
|
||||
new CodeBlock(),
|
||||
new HardBreak(),
|
||||
new Heading({ maxLevel: 6 }),
|
||||
new HorizontalRule(),
|
||||
new Image(),
|
||||
|
||||
new Table(),
|
||||
new TableHead(),
|
||||
new TableBody(),
|
||||
new TableHeaderRow(),
|
||||
new TableRow(),
|
||||
new TableCell(),
|
||||
|
||||
new Emoji(),
|
||||
new Reference(),
|
||||
|
||||
new TableOfContents(),
|
||||
new Video(),
|
||||
|
||||
new BulletList(),
|
||||
new OrderedList(),
|
||||
new ListItem(),
|
||||
|
||||
new DescriptionList(),
|
||||
new DescriptionTerm(),
|
||||
new DescriptionDetails(),
|
||||
|
||||
new TaskList(),
|
||||
new OrderedTaskList(),
|
||||
new TaskListItem(),
|
||||
|
||||
new Summary(),
|
||||
new Details(),
|
||||
|
||||
new Bold(),
|
||||
new Italic(),
|
||||
new Strike(),
|
||||
new InlineDiff(),
|
||||
|
||||
new Link(),
|
||||
new Code(),
|
||||
new MathMark(),
|
||||
new InlineHTML(),
|
||||
];
|
11
app/assets/javascripts/behaviors/markdown/marks/bold.js
Normal file
11
app/assets/javascripts/behaviors/markdown/marks/bold.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Bold as BaseBold } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Bold extends BaseBold {
|
||||
get toMarkdown() {
|
||||
return defaultMarkdownSerializer.marks.strong;
|
||||
}
|
||||
}
|
11
app/assets/javascripts/behaviors/markdown/marks/code.js
Normal file
11
app/assets/javascripts/behaviors/markdown/marks/code.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Code as BaseCode } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Code extends BaseCode {
|
||||
get toMarkdown() {
|
||||
return defaultMarkdownSerializer.marks.code;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Mark } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::InlineDiffFilter
|
||||
export default class InlineDiff extends Mark {
|
||||
get name() {
|
||||
return 'inline_diff';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
addition: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
parseDOM: [
|
||||
{ tag: 'span.idiff.addition', attrs: { addition: true } },
|
||||
{ tag: 'span.idiff.deletion', attrs: { addition: false } },
|
||||
],
|
||||
toDOM: node => [
|
||||
'span',
|
||||
{ class: `idiff left right ${node.attrs.addition ? 'addition' : 'deletion'}` },
|
||||
0,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
get toMarkdown() {
|
||||
return {
|
||||
mixable: true,
|
||||
open(state, mark) {
|
||||
return mark.attrs.addition ? '{+' : '{-';
|
||||
},
|
||||
close(state, mark) {
|
||||
return mark.attrs.addition ? '+}' : '-}';
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Mark } from 'tiptap';
|
||||
import _ from 'underscore';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class InlineHTML extends Mark {
|
||||
get name() {
|
||||
return 'inline_html';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
excludes: '',
|
||||
attrs: {
|
||||
tag: {},
|
||||
title: { default: null },
|
||||
},
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'sup, sub, kbd, q, samp, var',
|
||||
getAttrs: el => ({ tag: el.nodeName.toLowerCase() }),
|
||||
},
|
||||
{
|
||||
tag: 'abbr',
|
||||
getAttrs: el => ({ tag: 'abbr', title: el.getAttribute('title') }),
|
||||
},
|
||||
],
|
||||
toDOM: node => [node.attrs.tag, { title: node.attrs.title }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
get toMarkdown() {
|
||||
return {
|
||||
mixable: true,
|
||||
open(state, mark) {
|
||||
return `<${mark.attrs.tag}${
|
||||
mark.attrs.title ? ` title="${state.esc(_.escape(mark.attrs.title))}"` : ''
|
||||
}>`;
|
||||
},
|
||||
close(state, mark) {
|
||||
return `</${mark.attrs.tag}>`;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
11
app/assets/javascripts/behaviors/markdown/marks/italic.js
Normal file
11
app/assets/javascripts/behaviors/markdown/marks/italic.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Italic as BaseItalic } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Italic extends BaseItalic {
|
||||
get toMarkdown() {
|
||||
return defaultMarkdownSerializer.marks.em;
|
||||
}
|
||||
}
|
21
app/assets/javascripts/behaviors/markdown/marks/link.js
Normal file
21
app/assets/javascripts/behaviors/markdown/marks/link.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Link as BaseLink } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Link extends BaseLink {
|
||||
get toMarkdown() {
|
||||
return {
|
||||
mixable: true,
|
||||
open(state, mark, parent, index) {
|
||||
const open = defaultMarkdownSerializer.marks.link.open(state, mark, parent, index);
|
||||
return open === '<' ? '' : open;
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
const close = defaultMarkdownSerializer.marks.link.close(state, mark, parent, index);
|
||||
return close === '>' ? '' : close;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
41
app/assets/javascripts/behaviors/markdown/marks/math.js
Normal file
41
app/assets/javascripts/behaviors/markdown/marks/math.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Mark } from 'tiptap';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MathFilter
|
||||
export default class MathMark extends Mark {
|
||||
get name() {
|
||||
return 'math';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
// Matches HTML generated by Banzai::Filter::MathFilter
|
||||
{
|
||||
tag: 'code.code.math[data-math-style=inline]',
|
||||
priority: 51,
|
||||
},
|
||||
// Matches HTML after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
|
||||
{
|
||||
tag: 'span.katex',
|
||||
contentElement: 'annotation[encoding="application/x-tex"]',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['code', { class: 'code math', 'data-math-style': 'inline' }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
get toMarkdown() {
|
||||
return {
|
||||
escape: false,
|
||||
open(state, mark, parent, index) {
|
||||
return `$${defaultMarkdownSerializer.marks.code.open(state, mark, parent, index)}`;
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
return `${defaultMarkdownSerializer.marks.code.close(state, mark, parent, index)}$`;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
15
app/assets/javascripts/behaviors/markdown/marks/strike.js
Normal file
15
app/assets/javascripts/behaviors/markdown/marks/strike.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Strike as BaseStrike } from 'tiptap-extensions';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Strike extends BaseStrike {
|
||||
get toMarkdown() {
|
||||
return {
|
||||
open: '~~',
|
||||
close: '~~',
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Blockquote as BaseBlockquote } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Blockquote extends BaseBlockquote {
|
||||
toMarkdown(state, node) {
|
||||
if (!node.childCount) return;
|
||||
|
||||
defaultMarkdownSerializer.nodes.blockquote(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { BulletList as BaseBulletList } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class BulletList extends BaseBulletList {
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.bullet_list(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { CodeBlock as BaseCodeBlock } from 'tiptap-extensions';
|
||||
|
||||
const PLAINTEXT_LANG = 'plaintext';
|
||||
|
||||
// Transforms generated HTML back to GFM for:
|
||||
// - Banzai::Filter::SyntaxHighlightFilter
|
||||
// - Banzai::Filter::MathFilter
|
||||
// - Banzai::Filter::MermaidFilter
|
||||
// - Banzai::Filter::SuggestionFilter
|
||||
export default class CodeBlock extends BaseCodeBlock {
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
group: 'block',
|
||||
code: true,
|
||||
defining: true,
|
||||
attrs: {
|
||||
lang: { default: PLAINTEXT_LANG },
|
||||
},
|
||||
parseDOM: [
|
||||
// Matches HTML generated by Banzai::Filter::SyntaxHighlightFilter, Banzai::Filter::MathFilter, Banzai::Filter::MermaidFilter, or Banzai::Filter::SuggestionFilter
|
||||
{
|
||||
tag: 'pre.code.highlight',
|
||||
preserveWhitespace: 'full',
|
||||
getAttrs: el => {
|
||||
const lang = el.getAttribute('lang');
|
||||
if (!lang || lang === '') return {};
|
||||
|
||||
return { lang };
|
||||
},
|
||||
},
|
||||
// Matches HTML generated by Banzai::Filter::MathFilter,
|
||||
// after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
|
||||
{
|
||||
tag: 'span.katex-display',
|
||||
preserveWhitespace: 'full',
|
||||
contentElement: 'annotation[encoding="application/x-tex"]',
|
||||
attrs: { lang: 'math' },
|
||||
},
|
||||
// Matches HTML generated by Banzai::Filter::MermaidFilter,
|
||||
// after being transformed by app/assets/javascripts/behaviors/markdown/render_mermaid.js
|
||||
{
|
||||
tag: 'svg.mermaid',
|
||||
preserveWhitespace: 'full',
|
||||
contentElement: 'text.source',
|
||||
attrs: { lang: 'mermaid' },
|
||||
},
|
||||
// Matches HTML generated by Banzai::Filter::SuggestionFilter,
|
||||
// after being transformed by app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
|
||||
{
|
||||
tag: '.md-suggestion',
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
tag: '.md-suggestion-header',
|
||||
ignore: true,
|
||||
},
|
||||
{
|
||||
tag: '.md-suggestion-diff',
|
||||
preserveWhitespace: 'full',
|
||||
getContent: (el, schema) =>
|
||||
[...el.querySelectorAll('.line_content.new span')].map(span =>
|
||||
schema.text(span.innerText),
|
||||
),
|
||||
attrs: { lang: 'suggestion' },
|
||||
},
|
||||
],
|
||||
toDOM: node => ['pre', { class: 'code highlight', lang: node.attrs.lang }, ['code', 0]],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
if (!node.childCount) return;
|
||||
|
||||
const {
|
||||
textContent: text,
|
||||
attrs: { lang },
|
||||
} = node;
|
||||
|
||||
// Prefixes lines with 4 spaces if the code contains a line that starts with triple backticks
|
||||
if (lang === PLAINTEXT_LANG && text.match(/^```/gm)) {
|
||||
state.wrapBlock(' ', null, node, () => state.text(text, false));
|
||||
return;
|
||||
}
|
||||
|
||||
state.write('```');
|
||||
if (lang !== PLAINTEXT_LANG) state.write(lang);
|
||||
|
||||
state.ensureNewLine();
|
||||
state.text(text, false);
|
||||
state.ensureNewLine();
|
||||
|
||||
state.write('```');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class DescriptionDetails extends Node {
|
||||
get name() {
|
||||
return 'description_details';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
defining: true,
|
||||
parseDOM: [{ tag: 'dd' }],
|
||||
toDOM: () => ['dd', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.flushClose(1);
|
||||
state.write('<dd>');
|
||||
state.text(node.textContent, false);
|
||||
state.write('</dd>');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class DescriptionList extends Node {
|
||||
get name() {
|
||||
return 'description_list';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: '(description_term+ description_details+)+',
|
||||
group: 'block',
|
||||
parseDOM: [{ tag: 'dl' }],
|
||||
toDOM: () => ['dl', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write('<dl>\n');
|
||||
state.wrapBlock(' ', null, node, () => state.renderContent(node));
|
||||
state.flushClose(1);
|
||||
state.ensureNewLine();
|
||||
state.write('</dl>');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class DescriptionTerm extends Node {
|
||||
get name() {
|
||||
return 'description_term';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
defining: true,
|
||||
parseDOM: [{ tag: 'dt' }],
|
||||
toDOM: () => ['dt', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.flushClose(state.closed && state.closed.type === node.type ? 1 : 2);
|
||||
state.write('<dt>');
|
||||
state.text(node.textContent, false);
|
||||
state.write('</dt>');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
28
app/assets/javascripts/behaviors/markdown/nodes/details.js
Normal file
28
app/assets/javascripts/behaviors/markdown/nodes/details.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Details extends Node {
|
||||
get name() {
|
||||
return 'details';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'summary block*',
|
||||
group: 'block',
|
||||
parseDOM: [{ tag: 'details' }],
|
||||
toDOM: () => ['details', { open: true, onclick: 'return false', tabindex: '-1' }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write('<details>\n');
|
||||
state.renderContent(node);
|
||||
state.flushClose(1);
|
||||
state.ensureNewLine();
|
||||
state.write('</details>');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
15
app/assets/javascripts/behaviors/markdown/nodes/doc.js
Normal file
15
app/assets/javascripts/behaviors/markdown/nodes/doc.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
export default class Doc extends Node {
|
||||
get name() {
|
||||
return 'doc';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'block+',
|
||||
};
|
||||
}
|
||||
}
|
41
app/assets/javascripts/behaviors/markdown/nodes/emoji.js
Normal file
41
app/assets/javascripts/behaviors/markdown/nodes/emoji.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::EmojiFilter
|
||||
export default class Emoji extends Node {
|
||||
get name() {
|
||||
return 'emoji';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
attrs: {
|
||||
name: {},
|
||||
title: {},
|
||||
moji: {},
|
||||
},
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'gl-emoji',
|
||||
getAttrs: el => ({
|
||||
name: el.dataset.name,
|
||||
title: el.getAttribute('title'),
|
||||
moji: el.textContent,
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => [
|
||||
'gl-emoji',
|
||||
{ 'data-name': node.attrs.name, title: node.attrs.title },
|
||||
node.attrs.moji,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write(`:${node.attrs.name}:`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { HardBreak as BaseHardBreak } from 'tiptap-extensions';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class HardBreak extends BaseHardBreak {
|
||||
toMarkdown(state) {
|
||||
if (!state.atBlank()) state.write(' \n');
|
||||
}
|
||||
}
|
13
app/assets/javascripts/behaviors/markdown/nodes/heading.js
Normal file
13
app/assets/javascripts/behaviors/markdown/nodes/heading.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Heading as BaseHeading } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Heading extends BaseHeading {
|
||||
toMarkdown(state, node) {
|
||||
if (!node.childCount) return;
|
||||
|
||||
defaultMarkdownSerializer.nodes.heading(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { HorizontalRule as BaseHorizontalRule } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class HorizontalRule extends BaseHorizontalRule {
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.horizontal_rule(state, node);
|
||||
}
|
||||
}
|
52
app/assets/javascripts/behaviors/markdown/nodes/image.js
Normal file
52
app/assets/javascripts/behaviors/markdown/nodes/image.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Image as BaseImage } from 'tiptap-extensions';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
export default class Image extends BaseImage {
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
src: {},
|
||||
alt: {
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
group: 'inline',
|
||||
inline: true,
|
||||
draggable: true,
|
||||
parseDOM: [
|
||||
// Matches HTML generated by Banzai::Filter::ImageLinkFilter
|
||||
{
|
||||
tag: 'a.no-attachment-icon',
|
||||
priority: 51,
|
||||
skip: true,
|
||||
},
|
||||
// Matches HTML generated by Banzai::Filter::ImageLazyLoadFilter
|
||||
{
|
||||
tag: 'img[src]',
|
||||
getAttrs: el => {
|
||||
const imageSrc = el.src;
|
||||
const imageUrl =
|
||||
imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
|
||||
|
||||
return {
|
||||
src: imageUrl,
|
||||
title: el.getAttribute('title'),
|
||||
alt: el.getAttribute('alt'),
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
toDOM: node => ['img', node.attrs],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.image(state, node);
|
||||
}
|
||||
}
|
11
app/assets/javascripts/behaviors/markdown/nodes/list_item.js
Normal file
11
app/assets/javascripts/behaviors/markdown/nodes/list_item.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { ListItem as BaseListItem } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class ListItem extends BaseListItem {
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.list_item(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { OrderedList as BaseOrderedList } from 'tiptap-extensions';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class OrderedList extends BaseOrderedList {
|
||||
toMarkdown(state, node) {
|
||||
state.renderList(node, ' ', () => '1. ');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
|
||||
export default class OrderedTaskList extends Node {
|
||||
get name() {
|
||||
return 'ordered_task_list';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'block',
|
||||
content: '(task_list_item|list_item)+',
|
||||
parseDOM: [
|
||||
{
|
||||
priority: 51,
|
||||
tag: 'ol.task-list',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['ol', { class: 'task-list' }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.renderList(node, ' ', () => '1. ');
|
||||
}
|
||||
}
|
24
app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
Normal file
24
app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Paragraph extends Node {
|
||||
get name() {
|
||||
return 'paragraph';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
parseDOM: [{ tag: 'p' }],
|
||||
toDOM: () => ['p', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.paragraph(state, node);
|
||||
}
|
||||
}
|
52
app/assets/javascripts/behaviors/markdown/nodes/reference.js
Normal file
52
app/assets/javascripts/behaviors/markdown/nodes/reference.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::ReferenceFilter and subclasses
|
||||
export default class Reference extends Node {
|
||||
get name() {
|
||||
return 'reference';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
atom: true,
|
||||
attrs: {
|
||||
className: {},
|
||||
referenceType: {},
|
||||
originalText: { default: null },
|
||||
href: {},
|
||||
text: {},
|
||||
},
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'a.gfm:not([data-link=true])',
|
||||
priority: 51,
|
||||
getAttrs: el => ({
|
||||
className: el.className,
|
||||
referenceType: el.dataset.referenceType,
|
||||
originalText: el.dataset.original,
|
||||
href: el.getAttribute('href'),
|
||||
text: el.textContent,
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => [
|
||||
'a',
|
||||
{
|
||||
class: node.attrs.className,
|
||||
href: node.attrs.href,
|
||||
'data-reference-type': node.attrs.referenceType,
|
||||
'data-original': node.attrs.originalText,
|
||||
},
|
||||
node.attrs.text,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write(node.attrs.originalText || node.attrs.text);
|
||||
}
|
||||
}
|
27
app/assets/javascripts/behaviors/markdown/nodes/summary.js
Normal file
27
app/assets/javascripts/behaviors/markdown/nodes/summary.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Summary extends Node {
|
||||
get name() {
|
||||
return 'summary';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
defining: true,
|
||||
parseDOM: [{ tag: 'summary' }],
|
||||
toDOM: () => ['summary', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write('<summary>');
|
||||
state.text(node.textContent, false);
|
||||
state.write('</summary>');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
25
app/assets/javascripts/behaviors/markdown/nodes/table.js
Normal file
25
app/assets/javascripts/behaviors/markdown/nodes/table.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Table extends Node {
|
||||
get name() {
|
||||
return 'table';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'table_head table_body',
|
||||
group: 'block',
|
||||
isolating: true,
|
||||
parseDOM: [{ tag: 'table' }],
|
||||
toDOM: () => ['table', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.renderContent(node);
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class TableBody extends Node {
|
||||
get name() {
|
||||
return 'table_body';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'table_row+',
|
||||
parseDOM: [{ tag: 'tbody' }],
|
||||
toDOM: () => ['tbody', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.flushClose(1);
|
||||
state.renderContent(node);
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class TableCell extends Node {
|
||||
get name() {
|
||||
return 'table_cell';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
header: { default: false },
|
||||
align: { default: null },
|
||||
},
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'td, th',
|
||||
getAttrs: el => ({
|
||||
header: el.tagName === 'TH',
|
||||
align: el.getAttribute('align') || el.style.textAlign,
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => [node.attrs.header ? 'th' : 'td', { align: node.attrs.align }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.renderInline(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class TableHead extends Node {
|
||||
get name() {
|
||||
return 'table_head';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'table_header_row',
|
||||
parseDOM: [{ tag: 'thead' }],
|
||||
toDOM: () => ['thead', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.flushClose(1);
|
||||
state.renderContent(node);
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import TableRow from './table_row';
|
||||
|
||||
const CENTER_ALIGN = 'center';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class TableHeaderRow extends TableRow {
|
||||
get name() {
|
||||
return 'table_header_row';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'table_cell+',
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'thead tr',
|
||||
priority: 51,
|
||||
},
|
||||
],
|
||||
toDOM: () => ['tr', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
const cellWidths = super.toMarkdown(state, node);
|
||||
|
||||
state.flushClose(1);
|
||||
|
||||
state.write('|');
|
||||
node.forEach((cell, _, i) => {
|
||||
if (i) state.write('|');
|
||||
|
||||
state.write(cell.attrs.align === CENTER_ALIGN ? ':' : '-');
|
||||
state.write(state.repeat('-', cellWidths[i]));
|
||||
state.write(cell.attrs.align === CENTER_ALIGN || cell.attrs.align === 'right' ? ':' : '-');
|
||||
});
|
||||
state.write('|');
|
||||
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter
|
||||
export default class TableOfContents extends Node {
|
||||
get name() {
|
||||
return 'table_of_contents';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'block',
|
||||
atom: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'ul.section-nav',
|
||||
priority: 51,
|
||||
},
|
||||
{
|
||||
tag: 'p.table-of-contents',
|
||||
priority: 51,
|
||||
},
|
||||
],
|
||||
toDOM: () => ['p', { class: 'table-of-contents' }, 'Table of Contents'],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write('[[_TOC_]]');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
38
app/assets/javascripts/behaviors/markdown/nodes/table_row.js
Normal file
38
app/assets/javascripts/behaviors/markdown/nodes/table_row.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class TableRow extends Node {
|
||||
get name() {
|
||||
return 'table_row';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'table_cell+',
|
||||
parseDOM: [{ tag: 'tr' }],
|
||||
toDOM: () => ['tr', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
const cellWidths = [];
|
||||
|
||||
state.flushClose(1);
|
||||
|
||||
state.write('| ');
|
||||
node.forEach((cell, _, i) => {
|
||||
if (i) state.write(' | ');
|
||||
|
||||
const { length } = state.out;
|
||||
state.render(cell, node, i);
|
||||
cellWidths.push(state.out.length - length);
|
||||
});
|
||||
state.write(' |');
|
||||
|
||||
state.closeBlock(node);
|
||||
|
||||
return cellWidths;
|
||||
}
|
||||
}
|
28
app/assets/javascripts/behaviors/markdown/nodes/task_list.js
Normal file
28
app/assets/javascripts/behaviors/markdown/nodes/task_list.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
|
||||
export default class TaskList extends Node {
|
||||
get name() {
|
||||
return 'task_list';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'block',
|
||||
content: '(task_list_item|list_item)+',
|
||||
parseDOM: [
|
||||
{
|
||||
priority: 51,
|
||||
tag: 'ul.task-list',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['ul', { class: 'task-list' }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.renderList(node, ' ', () => '* ');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
|
||||
export default class TaskListItem extends Node {
|
||||
get name() {
|
||||
return 'task_list_item';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
done: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
defining: true,
|
||||
draggable: false,
|
||||
content: 'paragraph block*',
|
||||
parseDOM: [
|
||||
{
|
||||
priority: 51,
|
||||
tag: 'li.task-list-item',
|
||||
getAttrs: el => {
|
||||
const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox');
|
||||
return { done: checkbox && checkbox.checked };
|
||||
},
|
||||
},
|
||||
],
|
||||
toDOM(node) {
|
||||
return [
|
||||
'li',
|
||||
{ class: 'task-list-item' },
|
||||
[
|
||||
'input',
|
||||
{ type: 'checkbox', class: 'task-list-item-checkbox', checked: node.attrs.done },
|
||||
],
|
||||
['div', { class: 'todo-content' }, 0],
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write(`[${node.attrs.done ? 'x' : ' '}] `);
|
||||
state.renderContent(node);
|
||||
}
|
||||
}
|
20
app/assets/javascripts/behaviors/markdown/nodes/text.js
Normal file
20
app/assets/javascripts/behaviors/markdown/nodes/text.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
export default class Text extends Node {
|
||||
get name() {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'inline',
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.text(state, node);
|
||||
}
|
||||
}
|
54
app/assets/javascripts/behaviors/markdown/nodes/video.js
Normal file
54
app/assets/javascripts/behaviors/markdown/nodes/video.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::VideoLinkFilter
|
||||
export default class Video extends Node {
|
||||
get name() {
|
||||
return 'video';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
src: {},
|
||||
alt: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: '.video-container',
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
tag: '.video-container p',
|
||||
priority: 51,
|
||||
ignore: true,
|
||||
},
|
||||
{
|
||||
tag: 'video[src]',
|
||||
getAttrs: el => ({ src: el.getAttribute('src'), alt: el.dataset.title }),
|
||||
},
|
||||
],
|
||||
toDOM: node => [
|
||||
'video',
|
||||
{
|
||||
src: node.attrs.src,
|
||||
width: '400',
|
||||
controls: true,
|
||||
'data-setup': '{}',
|
||||
'data-title': node.attrs.alt,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.image(state, node);
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
24
app/assets/javascripts/behaviors/markdown/schema.js
Normal file
24
app/assets/javascripts/behaviors/markdown/schema.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Schema } from 'prosemirror-model';
|
||||
import editorExtensions from './editor_extensions';
|
||||
|
||||
const nodes = editorExtensions
|
||||
.filter(extension => extension.type === 'node')
|
||||
.reduce(
|
||||
(ns, { name, schema }) => ({
|
||||
...ns,
|
||||
[name]: schema,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const marks = editorExtensions
|
||||
.filter(extension => extension.type === 'mark')
|
||||
.reduce(
|
||||
(ms, { name, schema }) => ({
|
||||
...ms,
|
||||
[name]: schema,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
export default new Schema({ nodes, marks });
|
24
app/assets/javascripts/behaviors/markdown/serializer.js
Normal file
24
app/assets/javascripts/behaviors/markdown/serializer.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { MarkdownSerializer } from 'prosemirror-markdown';
|
||||
import editorExtensions from './editor_extensions';
|
||||
|
||||
const nodes = editorExtensions
|
||||
.filter(extension => extension.type === 'node')
|
||||
.reduce(
|
||||
(ns, { name, toMarkdown }) => ({
|
||||
...ns,
|
||||
[name]: toMarkdown,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const marks = editorExtensions
|
||||
.filter(extension => extension.type === 'mark')
|
||||
.reduce(
|
||||
(ms, { name, toMarkdown }) => ({
|
||||
...ms,
|
||||
[name]: toMarkdown,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
export default new MarkdownSerializer(nodes, marks);
|
|
@ -28,16 +28,13 @@ MarkdownPreview.prototype.ajaxCache = {};
|
|||
|
||||
MarkdownPreview.prototype.showPreview = function($form) {
|
||||
var mdText;
|
||||
var markdownVersion;
|
||||
var url;
|
||||
var preview = $form.find('.js-md-preview');
|
||||
var url = preview.data('url');
|
||||
if (preview.hasClass('md-preview-loading')) {
|
||||
return;
|
||||
}
|
||||
|
||||
mdText = $form.find('textarea.markdown-area').val();
|
||||
markdownVersion = $form.attr('data-markdown-version');
|
||||
url = this.versionedPreviewPath(preview.data('url'), markdownVersion);
|
||||
|
||||
if (mdText.trim().length === 0) {
|
||||
preview.text(this.emptyMessage);
|
||||
|
@ -67,16 +64,6 @@ MarkdownPreview.prototype.showPreview = function($form) {
|
|||
}
|
||||
};
|
||||
|
||||
MarkdownPreview.prototype.versionedPreviewPath = function(markdownPreviewPath, markdownVersion) {
|
||||
if (typeof markdownVersion === 'undefined') {
|
||||
return markdownPreviewPath;
|
||||
}
|
||||
|
||||
return `${markdownPreviewPath}${
|
||||
markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
|
||||
}markdown_version=${markdownVersion}`;
|
||||
};
|
||||
|
||||
MarkdownPreview.prototype.fetchMarkdownPreview = function(text, url, success) {
|
||||
if (!url) {
|
||||
return;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import _ from 'underscore';
|
||||
import Sidebar from '../../right_sidebar';
|
||||
import Shortcuts from './shortcuts';
|
||||
import { CopyAsGFM } from '../markdown/copy_as_gfm';
|
||||
|
@ -63,28 +62,32 @@ export default class ShortcutsIssuable extends Shortcuts {
|
|||
}
|
||||
|
||||
const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
|
||||
const selected = CopyAsGFM.nodeToGFM(el);
|
||||
const blockquoteEl = document.createElement('blockquote');
|
||||
blockquoteEl.appendChild(el);
|
||||
CopyAsGFM.nodeToGFM(blockquoteEl)
|
||||
.then(text => {
|
||||
if (text.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selected.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
// If replyField already has some content, add a newline before our quote
|
||||
const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
|
||||
$replyField
|
||||
.val((a, current) => `${current}${separator}${text}\n\n`)
|
||||
.trigger('input')
|
||||
.trigger('change');
|
||||
|
||||
const quote = _.map(selected.split('\n'), val => `${`> ${val}`.trim()}\n`);
|
||||
// Trigger autosize
|
||||
const event = document.createEvent('Event');
|
||||
event.initEvent('autosize:update', true, false);
|
||||
$replyField.get(0).dispatchEvent(event);
|
||||
|
||||
// If replyField already has some content, add a newline before our quote
|
||||
const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
|
||||
$replyField
|
||||
.val((a, current) => `${current}${separator}${quote.join('')}\n`)
|
||||
.trigger('input')
|
||||
.trigger('change');
|
||||
// Focus the input field
|
||||
$replyField.focus();
|
||||
|
||||
// Trigger autosize
|
||||
const event = document.createEvent('Event');
|
||||
event.initEvent('autosize:update', true, false);
|
||||
$replyField.get(0).dispatchEvent(event);
|
||||
|
||||
// Focus the input field
|
||||
$replyField.focus();
|
||||
return false;
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
|||
class="board-card"
|
||||
@mousedown="mouseDown"
|
||||
@mousemove="mouseMove"
|
||||
@mouseup="showIssue($event);"
|
||||
@mouseup="showIssue($event)"
|
||||
>
|
||||
<issue-card-inner
|
||||
:list="list"
|
||||
|
|
|
@ -96,7 +96,7 @@ export default {
|
|||
<template>
|
||||
<div class="board-new-issue-form">
|
||||
<div class="board-card">
|
||||
<form @submit="submit($event);">
|
||||
<form @submit="submit($event)">
|
||||
<div v-if="error" class="flash-container">
|
||||
<div class="flash-alert">An error occurred. Please try again.</div>
|
||||
</div>
|
||||
|
|
|
@ -184,7 +184,7 @@ export default {
|
|||
:title="label.description"
|
||||
class="badge color-label append-right-4 prepend-top-4"
|
||||
type="button"
|
||||
@click="filterByLabel(label);"
|
||||
@click="filterByLabel(label)"
|
||||
>
|
||||
{{ label.title }}
|
||||
</button>
|
||||
|
|
|
@ -58,7 +58,7 @@ export default {
|
|||
v-if="activeTab === 'selected'"
|
||||
class="btn btn-default"
|
||||
type="button"
|
||||
@click="changeTab('all');"
|
||||
@click="changeTab('all')"
|
||||
>
|
||||
Open issues
|
||||
</button>
|
||||
|
|
|
@ -71,7 +71,7 @@ export default {
|
|||
<span class="inline add-issues-footer-to-list"> to list </span>
|
||||
<lists-dropdown />
|
||||
</div>
|
||||
<button class="btn btn-default float-right" type="button" @click="toggleModal(false);">
|
||||
<button class="btn btn-default float-right" type="button" @click="toggleModal(false)">
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
|
|
|
@ -58,7 +58,7 @@ export default {
|
|||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
@click="toggleModal(false);"
|
||||
@click="toggleModal(false)"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
|
|
|
@ -130,7 +130,7 @@ export default {
|
|||
<div
|
||||
:class="{ 'is-active': issue.selected }"
|
||||
class="board-card"
|
||||
@click="toggleIssue($event, issue);"
|
||||
@click="toggleIssue($event, issue)"
|
||||
>
|
||||
<issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" />
|
||||
<icon
|
||||
|
|
|
@ -38,7 +38,7 @@ export default {
|
|||
:class="{ 'is-active': list.id == selected.id }"
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="modal.selectedList = list;"
|
||||
@click.prevent="modal.selectedList = list"
|
||||
>
|
||||
<span :style="{ backgroundColor: list.label.color }" class="dropdown-label-box"> </span>
|
||||
{{ list.title }}
|
||||
|
|
|
@ -21,12 +21,12 @@ export default {
|
|||
<div class="top-area prepend-top-10 append-bottom-10">
|
||||
<ul class="nav-links issues-state-filters">
|
||||
<li :class="{ active: activeTab == 'all' }">
|
||||
<a href="#" role="button" @click.prevent="changeTab('all');">
|
||||
<a href="#" role="button" @click.prevent="changeTab('all')">
|
||||
Open issues <span class="badge badge-pill"> {{ issuesCount }} </span>
|
||||
</a>
|
||||
</li>
|
||||
<li :class="{ active: activeTab == 'selected' }">
|
||||
<a href="#" role="button" @click.prevent="changeTab('selected');">
|
||||
<a href="#" role="button" @click.prevent="changeTab('selected')">
|
||||
Selected issues <span class="badge badge-pill"> {{ selectedCount }} </span>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -82,7 +82,7 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<label class="label-bold prepend-top-10"> Project </label>
|
||||
<div ref="projectsDropdown" class="dropdown">
|
||||
<div ref="projectsDropdown" class="dropdown dropdown-projects">
|
||||
<button
|
||||
class="dropdown-menu-toggle wide"
|
||||
type="button"
|
||||
|
|
|
@ -6,7 +6,13 @@ import Flash from '../flash';
|
|||
import Poll from '../lib/utils/poll';
|
||||
import initSettingsPanels from '../settings_panels';
|
||||
import eventHub from './event_hub';
|
||||
import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
|
||||
import {
|
||||
APPLICATION_STATUS,
|
||||
REQUEST_SUBMITTED,
|
||||
REQUEST_FAILURE,
|
||||
UPGRADE_REQUESTED,
|
||||
UPGRADE_REQUEST_FAILURE,
|
||||
} from './constants';
|
||||
import ClustersService from './services/clusters_service';
|
||||
import ClustersStore from './stores/clusters_store';
|
||||
import Applications from './components/applications.vue';
|
||||
|
@ -120,11 +126,17 @@ export default class Clusters {
|
|||
addListeners() {
|
||||
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
|
||||
eventHub.$on('installApplication', this.installApplication);
|
||||
eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
|
||||
eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId));
|
||||
eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
|
||||
}
|
||||
|
||||
removeListeners() {
|
||||
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
|
||||
eventHub.$off('installApplication', this.installApplication);
|
||||
eventHub.$off('upgradeApplication', this.upgradeApplication);
|
||||
eventHub.$off('upgradeFailed', this.upgradeFailed);
|
||||
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
|
||||
}
|
||||
|
||||
initPolling() {
|
||||
|
@ -231,22 +243,33 @@ export default class Clusters {
|
|||
|
||||
installApplication(data) {
|
||||
const appId = data.id;
|
||||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
|
||||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUBMITTED);
|
||||
this.store.updateAppProperty(appId, 'requestReason', null);
|
||||
this.store.updateAppProperty(appId, 'statusReason', null);
|
||||
|
||||
this.service
|
||||
.installApplication(appId, data.params)
|
||||
.then(() => {
|
||||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
|
||||
})
|
||||
.catch(() => {
|
||||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
|
||||
this.store.updateAppProperty(
|
||||
appId,
|
||||
'requestReason',
|
||||
s__('ClusterIntegration|Request to begin installing failed'),
|
||||
);
|
||||
});
|
||||
this.service.installApplication(appId, data.params).catch(() => {
|
||||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
|
||||
this.store.updateAppProperty(
|
||||
appId,
|
||||
'requestReason',
|
||||
s__('ClusterIntegration|Request to begin installing failed'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
upgradeApplication(data) {
|
||||
const appId = data.id;
|
||||
this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED);
|
||||
this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
|
||||
this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId));
|
||||
}
|
||||
|
||||
upgradeFailed(appId) {
|
||||
this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE);
|
||||
}
|
||||
|
||||
dismissUpgradeSuccess(appId) {
|
||||
this.store.updateAppProperty(appId, 'requestStatus', null);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
<script>
|
||||
/* eslint-disable vue/require-default-prop */
|
||||
import { GlLink } from '@gitlab/ui';
|
||||
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
|
||||
import { s__, sprintf } from '../../locale';
|
||||
import eventHub from '../event_hub';
|
||||
import identicon from '../../vue_shared/components/identicon.vue';
|
||||
import loadingButton from '../../vue_shared/components/loading_button.vue';
|
||||
import {
|
||||
APPLICATION_STATUS,
|
||||
REQUEST_LOADING,
|
||||
REQUEST_SUCCESS,
|
||||
REQUEST_SUBMITTED,
|
||||
REQUEST_FAILURE,
|
||||
UPGRADE_REQUESTED,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
loadingButton,
|
||||
identicon,
|
||||
TimeagoTooltip,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
|
@ -59,6 +63,18 @@ export default {
|
|||
type: String,
|
||||
required: false,
|
||||
},
|
||||
version: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
chartRepo: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
upgradeAvailable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
installApplicationRequestParams: {
|
||||
type: Object,
|
||||
required: false,
|
||||
|
@ -72,11 +88,31 @@ export default {
|
|||
isKnownStatus() {
|
||||
return Object.values(APPLICATION_STATUS).includes(this.status);
|
||||
},
|
||||
isInstalling() {
|
||||
return (
|
||||
this.status === APPLICATION_STATUS.SCHEDULED ||
|
||||
this.status === APPLICATION_STATUS.INSTALLING ||
|
||||
(this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.isInstalled)
|
||||
);
|
||||
},
|
||||
isInstalled() {
|
||||
return (
|
||||
this.status === APPLICATION_STATUS.INSTALLED ||
|
||||
this.status === APPLICATION_STATUS.UPDATED ||
|
||||
this.status === APPLICATION_STATUS.UPDATING
|
||||
this.status === APPLICATION_STATUS.UPDATING ||
|
||||
this.status === APPLICATION_STATUS.UPDATE_ERRORED
|
||||
);
|
||||
},
|
||||
canInstall() {
|
||||
if (this.isInstalling) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
|
||||
this.status === APPLICATION_STATUS.INSTALLABLE ||
|
||||
this.status === APPLICATION_STATUS.ERROR ||
|
||||
this.isUnknownStatus
|
||||
);
|
||||
},
|
||||
hasLogo() {
|
||||
|
@ -90,12 +126,7 @@ export default {
|
|||
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
|
||||
);
|
||||
return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling;
|
||||
},
|
||||
installButtonDisabled() {
|
||||
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
|
||||
|
@ -104,30 +135,17 @@ export default {
|
|||
return (
|
||||
((this.status !== APPLICATION_STATUS.INSTALLABLE &&
|
||||
this.status !== APPLICATION_STATUS.ERROR) ||
|
||||
this.requestStatus === REQUEST_LOADING ||
|
||||
this.requestStatus === REQUEST_SUCCESS) &&
|
||||
this.isInstalling) &&
|
||||
this.isKnownStatus
|
||||
);
|
||||
},
|
||||
installButtonLabel() {
|
||||
let label;
|
||||
if (
|
||||
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
|
||||
this.status === APPLICATION_STATUS.INSTALLABLE ||
|
||||
this.status === APPLICATION_STATUS.ERROR ||
|
||||
this.isUnknownStatus
|
||||
) {
|
||||
if (this.canInstall) {
|
||||
label = s__('ClusterIntegration|Install');
|
||||
} else if (
|
||||
this.status === APPLICATION_STATUS.SCHEDULED ||
|
||||
this.status === APPLICATION_STATUS.INSTALLING
|
||||
) {
|
||||
} else if (this.isInstalling) {
|
||||
label = s__('ClusterIntegration|Installing');
|
||||
} else if (
|
||||
this.status === APPLICATION_STATUS.INSTALLED ||
|
||||
this.status === APPLICATION_STATUS.UPDATED ||
|
||||
this.status === APPLICATION_STATUS.UPDATING
|
||||
) {
|
||||
} else if (this.isInstalled) {
|
||||
label = s__('ClusterIntegration|Installed');
|
||||
}
|
||||
|
||||
|
@ -140,13 +158,79 @@ export default {
|
|||
return s__('ClusterIntegration|Manage');
|
||||
},
|
||||
hasError() {
|
||||
return this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE;
|
||||
return (
|
||||
!this.isInstalling &&
|
||||
(this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE)
|
||||
);
|
||||
},
|
||||
generalErrorDescription() {
|
||||
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
|
||||
title: this.title,
|
||||
});
|
||||
},
|
||||
versionLabel() {
|
||||
if (this.upgradeFailed) {
|
||||
return s__('ClusterIntegration|Upgrade failed');
|
||||
} else if (this.isUpgrading) {
|
||||
return s__('ClusterIntegration|Upgrading');
|
||||
}
|
||||
|
||||
return s__('ClusterIntegration|Upgraded');
|
||||
},
|
||||
upgradeRequested() {
|
||||
return this.requestStatus === UPGRADE_REQUESTED;
|
||||
},
|
||||
upgradeSuccessful() {
|
||||
return this.status === APPLICATION_STATUS.UPDATED;
|
||||
},
|
||||
upgradeFailed() {
|
||||
if (this.isUpgrading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
|
||||
},
|
||||
upgradeFailureDescription() {
|
||||
return sprintf(
|
||||
s__(
|
||||
'ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again.',
|
||||
),
|
||||
{
|
||||
title: this.title,
|
||||
},
|
||||
);
|
||||
},
|
||||
upgradeSuccessDescription() {
|
||||
return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), {
|
||||
title: this.title,
|
||||
});
|
||||
},
|
||||
upgradeButtonLabel() {
|
||||
let label;
|
||||
if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) {
|
||||
label = s__('ClusterIntegration|Upgrade');
|
||||
} else if (this.isUpgrading) {
|
||||
label = s__('ClusterIntegration|Upgrading');
|
||||
} else if (this.upgradeFailed) {
|
||||
label = s__('ClusterIntegration|Retry upgrade');
|
||||
}
|
||||
|
||||
return label;
|
||||
},
|
||||
isUpgrading() {
|
||||
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
|
||||
return (
|
||||
this.status === APPLICATION_STATUS.UPDATING ||
|
||||
(this.upgradeRequested && !this.upgradeSuccessful)
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
status() {
|
||||
if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) {
|
||||
eventHub.$emit('upgradeFailed', this.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
installClicked() {
|
||||
|
@ -155,6 +239,15 @@ export default {
|
|||
params: this.installApplicationRequestParams,
|
||||
});
|
||||
},
|
||||
upgradeClicked() {
|
||||
eventHub.$emit('upgradeApplication', {
|
||||
id: this.id,
|
||||
params: this.installApplicationRequestParams,
|
||||
});
|
||||
},
|
||||
dismissUpgradeSuccess() {
|
||||
eventHub.$emit('dismissUpgradeSuccess', this.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -208,6 +301,51 @@ export default {
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="(upgradeSuccessful || upgradeFailed) && !upgradeAvailable"
|
||||
class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
|
||||
>
|
||||
{{ versionLabel }}
|
||||
|
||||
<span v-if="upgradeSuccessful"> to</span>
|
||||
|
||||
<gl-link
|
||||
v-if="upgradeSuccessful"
|
||||
:href="chartRepo"
|
||||
target="_blank"
|
||||
class="js-cluster-application-upgrade-version"
|
||||
>
|
||||
chart v{{ version }}
|
||||
</gl-link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="upgradeFailed && !isUpgrading"
|
||||
class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
|
||||
>
|
||||
{{ upgradeFailureDescription }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="upgradeRequested && upgradeSuccessful"
|
||||
class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3"
|
||||
>
|
||||
{{ upgradeSuccessDescription }}
|
||||
|
||||
<button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<loading-button
|
||||
v-if="upgradeAvailable || upgradeFailed || isUpgrading"
|
||||
class="btn btn-primary js-cluster-application-upgrade-button mt-2"
|
||||
:loading="isUpgrading"
|
||||
:disabled="isUpgrading"
|
||||
:label="upgradeButtonLabel"
|
||||
@click="upgradeClicked"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
|
||||
|
|
|
@ -362,6 +362,9 @@ export default {
|
|||
:status-reason="applications.runner.statusReason"
|
||||
:request-status="applications.runner.requestStatus"
|
||||
:request-reason="applications.runner.requestReason"
|
||||
:version="applications.runner.version"
|
||||
:chart-repo="applications.runner.chartRepo"
|
||||
:upgrade-available="applications.runner.upgradeAvailable"
|
||||
:disabled="!helmInstalled"
|
||||
title-link="https://docs.gitlab.com/runner/"
|
||||
>
|
||||
|
|
|
@ -12,16 +12,19 @@ export const APPLICATION_STATUS = {
|
|||
SCHEDULED: 'scheduled',
|
||||
INSTALLING: 'installing',
|
||||
INSTALLED: 'installed',
|
||||
UPDATED: 'updated',
|
||||
UPDATING: 'updating',
|
||||
UPDATED: 'updated',
|
||||
UPDATE_ERRORED: 'update_errored',
|
||||
ERROR: 'errored',
|
||||
};
|
||||
|
||||
// These are only used client-side
|
||||
export const REQUEST_LOADING = 'request-loading';
|
||||
export const REQUEST_SUCCESS = 'request-success';
|
||||
export const REQUEST_SUBMITTED = 'request-submitted';
|
||||
export const REQUEST_FAILURE = 'request-failure';
|
||||
export const UPGRADE_REQUESTED = 'upgrade-requested';
|
||||
export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure';
|
||||
export const INGRESS = 'ingress';
|
||||
export const JUPYTER = 'jupyter';
|
||||
export const KNATIVE = 'knative';
|
||||
export const RUNNER = 'runner';
|
||||
export const CERT_MANAGER = 'cert_manager';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { s__ } from '../../locale';
|
||||
import { parseBoolean } from '../../lib/utils/common_utils';
|
||||
import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER } from '../constants';
|
||||
import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER, RUNNER } from '../constants';
|
||||
|
||||
export default class ClusterStore {
|
||||
constructor() {
|
||||
|
@ -40,6 +40,9 @@ export default class ClusterStore {
|
|||
statusReason: null,
|
||||
requestStatus: null,
|
||||
requestReason: null,
|
||||
version: null,
|
||||
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
|
||||
upgradeAvailable: null,
|
||||
},
|
||||
prometheus: {
|
||||
title: s__('ClusterIntegration|Prometheus'),
|
||||
|
@ -100,7 +103,13 @@ export default class ClusterStore {
|
|||
this.state.statusReason = serverState.status_reason;
|
||||
|
||||
serverState.applications.forEach(serverAppEntry => {
|
||||
const { name: appId, status, status_reason: statusReason } = serverAppEntry;
|
||||
const {
|
||||
name: appId,
|
||||
status,
|
||||
status_reason: statusReason,
|
||||
version,
|
||||
update_available: upgradeAvailable,
|
||||
} = serverAppEntry;
|
||||
|
||||
this.state.applications[appId] = {
|
||||
...(this.state.applications[appId] || {}),
|
||||
|
@ -124,6 +133,9 @@ export default class ClusterStore {
|
|||
serverAppEntry.hostname || this.state.applications.knative.hostname;
|
||||
this.state.applications.knative.externalIp =
|
||||
serverAppEntry.external_ip || this.state.applications.knative.externalIp;
|
||||
} else if (appId === RUNNER) {
|
||||
this.state.applications.runner.version = version;
|
||||
this.state.applications.runner.upgradeAvailable = upgradeAvailable;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
1
app/assets/javascripts/commons/jquery.js
vendored
1
app/assets/javascripts/commons/jquery.js
vendored
|
@ -7,4 +7,3 @@ import 'vendor/jquery.caret';
|
|||
import 'vendor/jquery.atwho';
|
||||
import 'vendor/jquery.scrollTo';
|
||||
import 'jquery.waitforimages';
|
||||
import 'select2/select2';
|
||||
|
|
|
@ -13,6 +13,9 @@ export default class ContextualSidebar {
|
|||
initDomElements() {
|
||||
this.$page = $('.layout-page');
|
||||
this.$sidebar = $('.nav-sidebar');
|
||||
|
||||
if (!this.$sidebar.length) return;
|
||||
|
||||
this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
|
||||
this.$overlay = $('.mobile-overlay');
|
||||
this.$openSidebar = $('.toggle-mobile-nav');
|
||||
|
@ -21,12 +24,14 @@ export default class ContextualSidebar {
|
|||
}
|
||||
|
||||
bindEvents() {
|
||||
if (!this.$sidebar.length) return;
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (
|
||||
!e.target.closest('.nav-sidebar') &&
|
||||
(bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')
|
||||
) {
|
||||
this.toggleCollapsedSidebar(true);
|
||||
this.toggleCollapsedSidebar(true, true);
|
||||
}
|
||||
});
|
||||
this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
|
||||
|
@ -34,7 +39,7 @@ export default class ContextualSidebar {
|
|||
this.$overlay.on('click', () => this.toggleSidebarNav(false));
|
||||
this.$sidebarToggle.on('click', () => {
|
||||
const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop');
|
||||
this.toggleCollapsedSidebar(value);
|
||||
this.toggleCollapsedSidebar(value, true);
|
||||
});
|
||||
|
||||
$(window).on('resize', () => _.debounce(this.render(), 100));
|
||||
|
@ -53,16 +58,19 @@ export default class ContextualSidebar {
|
|||
this.$sidebar.removeClass('sidebar-collapsed-desktop');
|
||||
}
|
||||
|
||||
toggleCollapsedSidebar(collapsed) {
|
||||
toggleCollapsedSidebar(collapsed, saveCookie) {
|
||||
const breakpoint = bp.getBreakpointSize();
|
||||
|
||||
if (this.$sidebar.length) {
|
||||
this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed);
|
||||
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
|
||||
}
|
||||
ContextualSidebar.setCollapsedCookie(collapsed);
|
||||
|
||||
this.toggleSidebarOverflow();
|
||||
if (saveCookie) {
|
||||
ContextualSidebar.setCollapsedCookie(collapsed);
|
||||
}
|
||||
|
||||
requestIdleCallback(() => this.toggleSidebarOverflow());
|
||||
}
|
||||
|
||||
toggleSidebarOverflow() {
|
||||
|
@ -74,13 +82,15 @@ export default class ContextualSidebar {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (!this.$sidebar.length) return;
|
||||
|
||||
const breakpoint = bp.getBreakpointSize();
|
||||
|
||||
if (breakpoint === 'sm' || breakpoint === 'md') {
|
||||
this.toggleCollapsedSidebar(true);
|
||||
this.toggleCollapsedSidebar(true, false);
|
||||
} else if (breakpoint === 'lg') {
|
||||
const collapse = parseBoolean(Cookies.get('sidebar_collapsed'));
|
||||
this.toggleCollapsedSidebar(collapse);
|
||||
this.toggleCollapsedSidebar(collapse, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ export default {
|
|||
<div class="gl-responsive-table-row deploy-key">
|
||||
<div class="table-section section-40">
|
||||
<div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Deploy key') }}</div>
|
||||
<div class="table-mobile-content">
|
||||
<div class="table-mobile-content qa-key">
|
||||
<strong class="title qa-key-title"> {{ deployKey.title }} </strong>
|
||||
<div class="fingerprint qa-key-fingerprint">{{ deployKey.fingerprint }}</div>
|
||||
</div>
|
||||
|
|
|
@ -129,6 +129,10 @@ export default {
|
|||
created() {
|
||||
this.adjustView();
|
||||
eventHub.$once('fetchedNotesData', this.setDiscussions);
|
||||
eventHub.$once('fetchDiffData', this.fetchData);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('fetchDiffData', this.fetchData);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['startTaskList']),
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import { polyfillSticky } from '~/lib/utils/sticky';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
|
||||
import SettingsDropdown from './settings_dropdown.vue';
|
||||
import DiffStats from './diff_stats.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -12,6 +14,8 @@ export default {
|
|||
Icon,
|
||||
GlLink,
|
||||
GlButton,
|
||||
SettingsDropdown,
|
||||
DiffStats,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -33,27 +37,33 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState('diffs', ['commit', 'showTreeList', 'startVersion', 'latestVersionPath']),
|
||||
...mapGetters('diffs', ['isInlineView', 'isParallelView', 'hasCollapsedFile']),
|
||||
...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']),
|
||||
...mapState('diffs', [
|
||||
'commit',
|
||||
'showTreeList',
|
||||
'startVersion',
|
||||
'latestVersionPath',
|
||||
'addedLines',
|
||||
'removedLines',
|
||||
]),
|
||||
comparableDiffs() {
|
||||
return this.mergeRequestDiffs.slice(1);
|
||||
},
|
||||
toggleWhitespaceText() {
|
||||
if (this.isWhitespaceVisible()) {
|
||||
return __('Hide whitespace changes');
|
||||
}
|
||||
return __('Show whitespace changes');
|
||||
},
|
||||
toggleWhitespacePath() {
|
||||
if (this.isWhitespaceVisible()) {
|
||||
return mergeUrlParams({ w: 1 }, window.location.href);
|
||||
}
|
||||
|
||||
return mergeUrlParams({ w: 0 }, window.location.href);
|
||||
},
|
||||
showDropdowns() {
|
||||
return !this.commit && this.mergeRequestDiffs.length;
|
||||
},
|
||||
fileTreeIcon() {
|
||||
return this.showTreeList ? 'collapse-left' : 'expand-left';
|
||||
},
|
||||
toggleFileBrowserTitle() {
|
||||
return this.showTreeList ? __('Hide file browser') : __('Show file browser');
|
||||
},
|
||||
baseVersionPath() {
|
||||
return this.mergeRequestDiff.base_version_path;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
polyfillSticky(this.$el);
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', [
|
||||
|
@ -62,15 +72,12 @@ export default {
|
|||
'expandAllFiles',
|
||||
'toggleShowTreeList',
|
||||
]),
|
||||
isWhitespaceVisible() {
|
||||
return getParameterValues('w')[0] !== '1';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mr-version-controls">
|
||||
<div class="mr-version-controls" :class="{ 'is-fileTreeOpen': showTreeList }">
|
||||
<div class="mr-version-menus-container content-block">
|
||||
<button
|
||||
v-gl-tooltip.hover
|
||||
|
@ -79,10 +86,10 @@ export default {
|
|||
:class="{
|
||||
active: showTreeList,
|
||||
}"
|
||||
:title="__('Toggle file browser')"
|
||||
:title="toggleFileBrowserTitle"
|
||||
@click="toggleShowTreeList"
|
||||
>
|
||||
<icon name="hamburger" />
|
||||
<icon :name="fileTreeIcon" />
|
||||
</button>
|
||||
<div v-if="showDropdowns" class="d-flex align-items-center compare-versions-container">
|
||||
Changes between
|
||||
|
@ -95,6 +102,7 @@ export default {
|
|||
and
|
||||
<compare-versions-dropdown
|
||||
:other-versions="comparableDiffs"
|
||||
:base-version-path="baseVersionPath"
|
||||
:start-version="startVersion"
|
||||
:target-branch="targetBranch"
|
||||
class="mr-version-compare-dropdown"
|
||||
|
@ -105,6 +113,11 @@ export default {
|
|||
<gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link>
|
||||
</div>
|
||||
<div class="inline-parallel-buttons d-none d-md-flex ml-auto">
|
||||
<diff-stats
|
||||
:diff-files-length="diffFilesLength"
|
||||
:added-lines="addedLines"
|
||||
:removed-lines="removedLines"
|
||||
/>
|
||||
<gl-button
|
||||
v-if="commit || startVersion"
|
||||
:href="latestVersionPath"
|
||||
|
@ -115,31 +128,7 @@ export default {
|
|||
<a v-show="hasCollapsedFile" class="btn btn-default append-right-8" @click="expandAllFiles">
|
||||
{{ __('Expand all') }}
|
||||
</a>
|
||||
<a :href="toggleWhitespacePath" class="btn btn-default qa-toggle-whitespace">
|
||||
{{ toggleWhitespaceText }}
|
||||
</a>
|
||||
<div class="btn-group prepend-left-8">
|
||||
<button
|
||||
id="inline-diff-btn"
|
||||
:class="{ active: isInlineView }"
|
||||
type="button"
|
||||
class="btn js-inline-diff-button"
|
||||
data-view-type="inline"
|
||||
@click="setInlineDiffViewType"
|
||||
>
|
||||
{{ __('Inline') }}
|
||||
</button>
|
||||
<button
|
||||
id="parallel-diff-btn"
|
||||
:class="{ active: isParallelView }"
|
||||
type="button"
|
||||
class="btn js-parallel-diff-button"
|
||||
data-view-type="parallel"
|
||||
@click="setParallelDiffViewType"
|
||||
>
|
||||
{{ __('Side-by-side') }}
|
||||
</button>
|
||||
</div>
|
||||
<settings-dropdown />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -34,14 +34,13 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
baseVersionPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
baseVersion() {
|
||||
return {
|
||||
name: 'hii',
|
||||
versionIndex: -1,
|
||||
};
|
||||
},
|
||||
targetVersions() {
|
||||
if (this.mergeRequestVersion) {
|
||||
return this.otherVersions;
|
||||
|
@ -62,6 +61,9 @@ export default {
|
|||
);
|
||||
},
|
||||
href(version) {
|
||||
if (this.isBase(version)) {
|
||||
return this.baseVersionPath;
|
||||
}
|
||||
if (this.showCommitCount) {
|
||||
return version.version_path;
|
||||
}
|
||||
|
@ -139,7 +141,7 @@ export default {
|
|||
<time-ago
|
||||
v-if="version.created_at"
|
||||
:time="version.created_at"
|
||||
class="js-timeago js-timeago-render"
|
||||
class="js-timeago"
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
|
|
|
@ -127,7 +127,7 @@ export default {
|
|||
:save-button-title="__('Comment')"
|
||||
class="diff-comment-form new-note discussion-form discussion-form-container"
|
||||
@handleFormUpdate="handleSaveNote"
|
||||
@cancelForm="closeDiffFileCommentForm(diffFile.file_hash);"
|
||||
@cancelForm="closeDiffFileCommentForm(diffFile.file_hash)"
|
||||
/>
|
||||
</div>
|
||||
</diff-viewer>
|
||||
|
|
|
@ -68,7 +68,7 @@ export default {
|
|||
}"
|
||||
type="button"
|
||||
class="js-diff-notes-toggle"
|
||||
@click="toggleDiscussion({ discussionId: discussion.id });"
|
||||
@click="toggleDiscussion({ discussionId: discussion.id })"
|
||||
>
|
||||
<icon v-if="discussion.expanded" name="collapse" class="collapse-icon" />
|
||||
<template v-else>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { GlTooltipDirective } from '@gitlab/ui';
|
|||
import { truncateSha } from '~/lib/utils/text_utility';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import EditButton from './edit_button.vue';
|
||||
import DiffStats from './diff_stats.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -16,6 +17,7 @@ export default {
|
|||
EditButton,
|
||||
Icon,
|
||||
FileIcon,
|
||||
DiffStats,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -145,7 +147,7 @@ export default {
|
|||
<div
|
||||
ref="header"
|
||||
class="js-file-title file-title file-title-flex-parent"
|
||||
@click="handleToggleFile($event, true);"
|
||||
@click="handleToggleFile($event, true)"
|
||||
>
|
||||
<div class="file-header-content">
|
||||
<icon
|
||||
|
@ -202,6 +204,7 @@ export default {
|
|||
v-if="!diffFile.submodule && addMergeRequestButtons"
|
||||
class="file-actions d-none d-sm-block"
|
||||
>
|
||||
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
|
||||
<template v-if="diffFile.blob && diffFile.blob.readable_text">
|
||||
<button
|
||||
:disabled="!diffHasDiscussions(diffFile)"
|
||||
|
|
|
@ -179,7 +179,7 @@ export default {
|
|||
v-if="lineNumber"
|
||||
:data-linenumber="lineNumber"
|
||||
:href="lineHref"
|
||||
@click="setHighlightedRow(lineCode);"
|
||||
@click="setHighlightedRow(lineCode)"
|
||||
>
|
||||
</a>
|
||||
<diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" />
|
||||
|
|
|
@ -28,6 +28,11 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
helpPagePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
@ -95,6 +100,7 @@ export default {
|
|||
:is-editing="true"
|
||||
:line-code="line.line_code"
|
||||
:line="line"
|
||||
:help-page-path="helpPagePath"
|
||||
save-button-title="Comment"
|
||||
class="diff-comment-form"
|
||||
@cancelForm="handleCancelCommentForm"
|
||||
|
|
52
app/assets/javascripts/diffs/components/diff_stats.vue
Normal file
52
app/assets/javascripts/diffs/components/diff_stats.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { n__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: { Icon },
|
||||
props: {
|
||||
addedLines: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
removedLines: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
diffFilesLength: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filesText() {
|
||||
return n__('File', 'Files', this.diffFilesLength);
|
||||
},
|
||||
isCompareVersionsHeader() {
|
||||
return Boolean(this.diffFilesLength);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="diff-stats"
|
||||
:class="{
|
||||
'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader,
|
||||
'd-inline-flex': !isCompareVersionsHeader,
|
||||
}"
|
||||
>
|
||||
<div v-if="diffFilesLength !== null" class="diff-stats-group">
|
||||
<icon name="doc-code" class="diff-stats-icon text-secondary" />
|
||||
<strong>{{ diffFilesLength }} {{ filesText }}</strong>
|
||||
</div>
|
||||
<div class="diff-stats-group cgreen">
|
||||
<icon name="file-addition" class="diff-stats-icon" /> <strong>{{ addedLines }}</strong>
|
||||
</div>
|
||||
<div class="diff-stats-group cred">
|
||||
<icon name="file-deletion" class="diff-stats-icon" /> <strong>{{ removedLines }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -97,7 +97,7 @@ export default {
|
|||
v-if="canComment"
|
||||
type="button"
|
||||
class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
|
||||
@click="clickedImage($event.offsetX, $event.offsetY);"
|
||||
@click="clickedImage($event.offsetX, $event.offsetY)"
|
||||
>
|
||||
<span class="sr-only"> {{ __('Add image comment') }} </span>
|
||||
</button>
|
||||
|
@ -109,7 +109,7 @@ export default {
|
|||
:disabled="!shouldToggleDiscussion"
|
||||
class="js-image-badge"
|
||||
type="button"
|
||||
@click="toggleDiscussion({ discussionId: discussion.id });"
|
||||
@click="toggleDiscussion({ discussionId: discussion.id })"
|
||||
>
|
||||
<icon v-if="showCommentIcon" name="image-comment-dark" />
|
||||
<template v-else>
|
||||
|
|
|
@ -54,6 +54,7 @@ export default {
|
|||
:diff-file-hash="diffFileHash"
|
||||
:line="line"
|
||||
:note-target-line="line"
|
||||
:help-page-path="helpPagePath"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -101,6 +101,7 @@ export default {
|
|||
:diff-file-hash="diffFileHash"
|
||||
:line="line.left"
|
||||
:note-target-line="line.left"
|
||||
:help-page-path="helpPagePath"
|
||||
line-position="left"
|
||||
/>
|
||||
</td>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
Icon,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
|
||||
...mapState('diffs', ['renderTreeList', 'showWhitespace']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', [
|
||||
'setInlineDiffViewType',
|
||||
'setParallelDiffViewType',
|
||||
'setRenderTreeList',
|
||||
'setShowWhitespace',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default js-show-diff-settings"
|
||||
data-toggle="dropdown"
|
||||
data-display="static"
|
||||
>
|
||||
<icon name="settings" /> <icon name="arrow-down" />
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right p-2 pt-3 pb-3">
|
||||
<div>
|
||||
<span class="bold d-block mb-1">{{ __('File browser') }}</span>
|
||||
<div class="btn-group d-flex">
|
||||
<gl-button
|
||||
:class="{ active: !renderTreeList }"
|
||||
class="w-100 js-list-view"
|
||||
@click="setRenderTreeList(false)"
|
||||
>
|
||||
{{ __('List view') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
:class="{ active: renderTreeList }"
|
||||
class="w-100 js-tree-view"
|
||||
@click="setRenderTreeList(true)"
|
||||
>
|
||||
{{ __('Tree view') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="bold d-block mb-1">{{ __('Compare changes') }}</span>
|
||||
<div class="btn-group d-flex js-diff-view-buttons">
|
||||
<gl-button
|
||||
id="inline-diff-btn"
|
||||
:class="{ active: isInlineView }"
|
||||
class="w-100 js-inline-diff-button"
|
||||
data-view-type="inline"
|
||||
@click="setInlineDiffViewType"
|
||||
>
|
||||
{{ __('Inline') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
id="parallel-diff-btn"
|
||||
:class="{ active: isParallelView }"
|
||||
class="w-100 js-parallel-diff-button"
|
||||
data-view-type="parallel"
|
||||
@click="setParallelDiffViewType"
|
||||
>
|
||||
{{ __('Side-by-side') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<label class="mb-0">
|
||||
<input
|
||||
id="show-whitespace"
|
||||
type="checkbox"
|
||||
:checked="showWhitespace"
|
||||
@change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
|
||||
/>
|
||||
{{ __('Show whitespace changes') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,13 +1,10 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import FileRow from '~/vue_shared/components/file_row.vue';
|
||||
import FileRowStats from './file_row_stats.vue';
|
||||
|
||||
const treeListStorageKey = 'mr_diff_tree_list';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -17,53 +14,42 @@ export default {
|
|||
FileRow,
|
||||
},
|
||||
data() {
|
||||
const treeListStored = localStorage.getItem(treeListStorageKey);
|
||||
const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true;
|
||||
|
||||
return {
|
||||
search: '',
|
||||
renderTreeList,
|
||||
focusSearch: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('diffs', ['tree', 'addedLines', 'removedLines']),
|
||||
...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
|
||||
...mapState('diffs', ['tree', 'renderTreeList']),
|
||||
...mapGetters('diffs', ['allBlobs']),
|
||||
filteredTreeList() {
|
||||
const search = this.search.toLowerCase().trim();
|
||||
|
||||
if (search === '') return this.renderTreeList ? this.tree : this.allBlobs;
|
||||
if (search === '' || this.$options.fuzzyFileFinderEnabled)
|
||||
return this.renderTreeList ? this.tree : this.allBlobs;
|
||||
|
||||
return this.allBlobs.filter(f => f.path.toLowerCase().indexOf(search) >= 0);
|
||||
},
|
||||
rowDisplayTextKey() {
|
||||
if (this.renderTreeList && this.search.trim() === '') {
|
||||
return 'name';
|
||||
}
|
||||
return this.allBlobs.reduce((acc, folder) => {
|
||||
const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0);
|
||||
|
||||
return 'path';
|
||||
if (tree.length) {
|
||||
return acc.concat({
|
||||
...folder,
|
||||
tree,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
|
||||
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']),
|
||||
clearSearch() {
|
||||
this.search = '';
|
||||
this.toggleFocusSearch(false);
|
||||
},
|
||||
toggleRenderTreeList(toggle) {
|
||||
this.renderTreeList = toggle;
|
||||
localStorage.setItem(treeListStorageKey, this.renderTreeList);
|
||||
},
|
||||
toggleFocusSearch(toggle) {
|
||||
this.focusSearch = toggle;
|
||||
},
|
||||
blurSearch() {
|
||||
if (this.search.trim() === '') {
|
||||
this.toggleFocusSearch(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl'}+P`,
|
||||
FileRowStats,
|
||||
diffTreeFiltering: gon.features && gon.features.diffTreeFiltering,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -72,54 +58,39 @@ export default {
|
|||
<div class="append-bottom-8 position-relative tree-list-search d-flex">
|
||||
<div class="flex-fill d-flex">
|
||||
<icon name="search" class="position-absolute tree-list-icon" />
|
||||
<input
|
||||
v-model="search"
|
||||
:placeholder="s__('MergeRequest|Filter files')"
|
||||
type="search"
|
||||
class="form-control"
|
||||
@focus="toggleFocusSearch(true);"
|
||||
@blur="blurSearch"
|
||||
/>
|
||||
<button
|
||||
v-show="search"
|
||||
:aria-label="__('Clear search')"
|
||||
type="button"
|
||||
class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<icon name="close" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-show="!focusSearch" class="btn-group prepend-left-8 tree-list-view-toggle">
|
||||
<button
|
||||
v-gl-tooltip.hover
|
||||
:aria-label="__('List view')"
|
||||
:title="__('List view')"
|
||||
:class="{
|
||||
active: !renderTreeList,
|
||||
}"
|
||||
class="btn btn-default pt-0 pb-0 d-flex align-items-center"
|
||||
type="button"
|
||||
@click="toggleRenderTreeList(false);"
|
||||
>
|
||||
<icon name="hamburger" />
|
||||
</button>
|
||||
<button
|
||||
v-gl-tooltip.hover
|
||||
:aria-label="__('Tree view')"
|
||||
:title="__('Tree view')"
|
||||
:class="{
|
||||
active: renderTreeList,
|
||||
}"
|
||||
class="btn btn-default pt-0 pb-0 d-flex align-items-center"
|
||||
type="button"
|
||||
@click="toggleRenderTreeList(true);"
|
||||
>
|
||||
<icon name="file-tree" />
|
||||
</button>
|
||||
<template v-if="$options.diffTreeFiltering">
|
||||
<input
|
||||
v-model="search"
|
||||
:placeholder="s__('MergeRequest|Filter files')"
|
||||
type="search"
|
||||
class="form-control"
|
||||
/>
|
||||
<button
|
||||
v-show="search"
|
||||
:aria-label="__('Clear search')"
|
||||
type="button"
|
||||
class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<icon name="close" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="form-control text-left text-secondary"
|
||||
@click="toggleFileFinder(true)"
|
||||
>
|
||||
{{ s__('MergeRequest|Search files') }}
|
||||
</button>
|
||||
<span
|
||||
class="position-absolute text-secondary diff-tree-search-shortcut"
|
||||
v-html="$options.shortcutKeyCharacter"
|
||||
></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tree-list-scroll">
|
||||
<div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll">
|
||||
<template v-if="filteredTreeList.length">
|
||||
<file-row
|
||||
v-for="file in filteredTreeList"
|
||||
|
@ -129,8 +100,6 @@ export default {
|
|||
:hide-extra-on-tree="true"
|
||||
:extra-component="$options.FileRowStats"
|
||||
:show-changed-icon="true"
|
||||
:display-text-key="rowDisplayTextKey"
|
||||
:should-truncate-start="true"
|
||||
@toggleTreeOpen="toggleTreeOpen"
|
||||
@clickFile="scrollToFile"
|
||||
/>
|
||||
|
@ -139,12 +108,22 @@ export default {
|
|||
{{ s__('MergeRequest|No files found') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-once class="pt-3 pb-3 text-center">
|
||||
{{ n__('%d changed file', '%d changed files', diffFilesLength) }}
|
||||
<div>
|
||||
<span class="cgreen"> {{ n__('%d addition', '%d additions', addedLines) }} </span>
|
||||
<span class="cred"> {{ n__('%d deleted', '%d deletions', removedLines) }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tree-list-blobs .file-row-name {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.diff-tree-search-shortcut {
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tree-list-icon:not(button) {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -32,3 +32,7 @@ export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
|
|||
export const MAX_LINES_TO_BE_RENDERED = 2000;
|
||||
|
||||
export const MR_TREE_SHOW_KEY = 'mr_tree_show';
|
||||
|
||||
export const TREE_TYPE = 'tree';
|
||||
export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list';
|
||||
export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace';
|
||||
|
|
|
@ -1,8 +1,60 @@
|
|||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
import FindFile from '~/vue_shared/components/file_finder/index.vue';
|
||||
import eventHub from '../notes/event_hub';
|
||||
import diffsApp from './components/app.vue';
|
||||
import { TREE_LIST_STORAGE_KEY } from './constants';
|
||||
|
||||
export default function initDiffsApp(store) {
|
||||
const fileFinderEl = document.getElementById('js-diff-file-finder');
|
||||
|
||||
if (fileFinderEl) {
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: fileFinderEl,
|
||||
store,
|
||||
computed: {
|
||||
...mapState('diffs', ['fileFinderVisible', 'isLoading']),
|
||||
...mapGetters('diffs', ['flatBlobsList']),
|
||||
},
|
||||
watch: {
|
||||
fileFinderVisible(newVal, oldVal) {
|
||||
if (newVal && !oldVal && !this.flatBlobsList.length) {
|
||||
eventHub.$emit('fetchDiffData');
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']),
|
||||
openFile(file) {
|
||||
window.mrTabs.tabShown('diffs');
|
||||
this.scrollToFile(file.path);
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(FindFile, {
|
||||
props: {
|
||||
files: this.flatBlobsList,
|
||||
visible: this.fileFinderVisible,
|
||||
loading: this.isLoading,
|
||||
showDiffStats: true,
|
||||
clearSearchOnClose: false,
|
||||
},
|
||||
on: {
|
||||
toggle: this.toggleFileFinder,
|
||||
click: this.openFile,
|
||||
},
|
||||
class: ['diff-file-finder'],
|
||||
style: {
|
||||
display: this.fileFinderVisible ? '' : 'none',
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el: '#js-diffs-app',
|
||||
name: 'MergeRequestDiffs',
|
||||
|
@ -26,6 +78,16 @@ export default function initDiffsApp(store) {
|
|||
activeTab: state => state.page.activeTab,
|
||||
}),
|
||||
},
|
||||
created() {
|
||||
const treeListStored = localStorage.getItem(TREE_LIST_STORAGE_KEY);
|
||||
const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true;
|
||||
|
||||
this.setRenderTreeList(renderTreeList);
|
||||
this.setShowWhitespace({ showWhitespace: getParameterValues('w')[0] !== '1' });
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['setRenderTreeList', 'setShowWhitespace']),
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('diffs-app', {
|
||||
props: {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue