New upstream version 11.8.0

This commit is contained in:
Sruthi Chandran 2019-03-02 22:35:43 +05:30
parent 591efccd8b
commit 0329642ba5
2015 changed files with 177081 additions and 29694 deletions

View file

@ -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
View file

@ -80,3 +80,4 @@ eslint-report.html
package-lock.json
/junit_*.xml
/coverage-frontend/
jsdoc/

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -143,6 +143,7 @@ Naming/FileName:
- XMPP
- XSRF
- XSS
- GRPC
# GitLab ###################################################################

View file

@ -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:

View file

@ -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)

View file

@ -1 +1 @@
1.12.2
1.20.0

View file

@ -1 +1 @@
1.3.1
1.5.0

View file

@ -1 +1 @@
8.0.2
8.3.1

25
Gemfile
View file

@ -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'

View file

@ -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)

View file

@ -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

View file

@ -1 +1 @@
11.7.5
11.8.0

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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(() => {});
}
}

View 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(),
];

View 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;
}
}

View 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;
}
}

View file

@ -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 ? '+}' : '-}';
},
};
}
}

View file

@ -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}>`;
},
};
}
}

View 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;
}
}

View 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;
},
};
}
}

View 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)}$`;
},
};
}
}

View 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,
};
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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 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);
}
}

View 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 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);
}
}

View 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 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);
}
}

View 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);
}
}

View 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+',
};
}
}

View 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}:`);
}
}

View file

@ -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');
}
}

View 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);
}
}

View file

@ -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);
}
}

View 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);
}
}

View 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);
}
}

View file

@ -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. ');
}
}

View 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 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. ');
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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;
}
}

View 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, ' ', () => '* ');
}
}

View file

@ -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);
}
}

View 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);
}
}

View 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);
}
}

View 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 });

View 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);

View file

@ -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;

View file

@ -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;
}

View file

@ -86,7 +86,7 @@ export default {
class="board-card"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event);"
@mouseup="showIssue($event)"
>
<issue-card-inner
:list="list"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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 }}

View file

@ -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>

View file

@ -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"

View file

@ -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() {

View file

@ -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">
&times;
</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 }"

View file

@ -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/"
>

View file

@ -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';

View file

@ -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;
}
});
}

View file

@ -7,4 +7,3 @@ import 'vendor/jquery.caret';
import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo';
import 'jquery.waitforimages';
import 'select2/select2';

View file

@ -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);
}
}
}

View file

@ -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>

View file

@ -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']),

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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)"

View file

@ -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" />

View file

@ -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"

View 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>

View file

@ -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>

View file

@ -54,6 +54,7 @@ export default {
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
:help-page-path="helpPagePath"
/>
</div>
</td>

View file

@ -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>

View file

@ -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>

View file

@ -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) ? '&#8984;' : '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>

View file

@ -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';

View file

@ -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