New upstream version 10.8.7+dfsg
This commit is contained in:
parent
67db7fce51
commit
52cfd360f9
1721 changed files with 45512 additions and 20206 deletions
6
.babelrc
6
.babelrc
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
|
||||
"env": {
|
||||
"karma": {
|
||||
"plugins": ["rewire"]
|
||||
},
|
||||
"coverage": {
|
||||
"plugins": [
|
||||
[
|
||||
|
@ -14,7 +17,8 @@
|
|||
{
|
||||
"process.env.BABEL_ENV": "coverage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"rewire"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,3 +9,4 @@ lib/gitlab/gitaly_client/operation_service.rb
|
|||
lib/gitlab/background_migration/*
|
||||
app/models/project_services/kubernetes_service.rb
|
||||
lib/gitlab/workhorse.rb
|
||||
lib/gitlab/ci/trace/chunked_io.rb
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -64,11 +64,15 @@ eslint-report.html
|
|||
/tags
|
||||
/tmp/*
|
||||
/vendor/bundle/*
|
||||
/vendor/gitaly-ruby
|
||||
/builds*
|
||||
/shared/*
|
||||
/.gitlab_workhorse_secret
|
||||
/webpack-report/
|
||||
/knapsack/
|
||||
/rspec_flaky/
|
||||
/locale/**/LC_MESSAGES
|
||||
/locale/**/*.time_stamp
|
||||
/.rspec
|
||||
/plugins/*
|
||||
/.gitlab_pages_secret
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
|
||||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
|
||||
|
||||
.dedicated-runner: &dedicated-runner
|
||||
retry: 1
|
||||
|
@ -6,10 +6,11 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git
|
|||
- gitlab-org
|
||||
|
||||
.default-cache: &default-cache
|
||||
key: "ruby-2.3.6-with-yarn"
|
||||
key: "ruby-2.3.7-debian-stretch-with-yarn"
|
||||
paths:
|
||||
- vendor/ruby
|
||||
- .yarn-cache/
|
||||
- vendor/gitaly-ruby
|
||||
|
||||
.push-cache: &push-cache
|
||||
cache:
|
||||
|
@ -110,7 +111,7 @@ stages:
|
|||
# Jobs that only need to pull cache
|
||||
.dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs-and-qa
|
||||
<<: *except-docs
|
||||
<<: *pull-cache
|
||||
dependencies:
|
||||
- setup-test-env
|
||||
|
@ -122,6 +123,10 @@ stages:
|
|||
variables:
|
||||
SETUP_DB: "false"
|
||||
|
||||
.dedicated-no-docs-and-no-qa-pull-cache-job: &dedicated-no-docs-and-no-qa-pull-cache-job
|
||||
<<: *dedicated-no-docs-pull-cache-job
|
||||
<<: *except-docs-and-qa
|
||||
|
||||
.rake-exec: &rake-exec
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
script:
|
||||
|
@ -222,7 +227,7 @@ stages:
|
|||
- master@gitlab/gitlab-ee
|
||||
|
||||
.gitlab-setup: &gitlab-setup
|
||||
<<: *dedicated-no-docs-pull-cache-job
|
||||
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
|
||||
<<: *use-pg
|
||||
variables:
|
||||
CREATE_DB_USER: "true"
|
||||
|
@ -262,12 +267,12 @@ stages:
|
|||
|
||||
# DB migration, rollback, and seed jobs
|
||||
.db-migrate-reset: &db-migrate-reset
|
||||
<<: *dedicated-no-docs-pull-cache-job
|
||||
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
|
||||
script:
|
||||
- bundle exec rake db:migrate:reset
|
||||
|
||||
.migration-paths: &migration-paths
|
||||
<<: *dedicated-no-docs-pull-cache-job
|
||||
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
|
||||
variables:
|
||||
CREATE_DB_USER: "true"
|
||||
script:
|
||||
|
@ -289,7 +294,6 @@ stages:
|
|||
# Trigger a package build in omnibus-gitlab repository
|
||||
#
|
||||
package-and-qa:
|
||||
<<: *dedicated-runner
|
||||
image: ruby:2.4-alpine
|
||||
before_script: []
|
||||
stage: build
|
||||
|
@ -364,10 +368,11 @@ update-tests-metadata:
|
|||
- rspec_flaky/
|
||||
policy: push
|
||||
script:
|
||||
- retry gem install fog-aws mime-types
|
||||
- retry gem install fog-aws mime-types activesupport
|
||||
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
|
||||
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
|
||||
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
|
||||
- FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH}
|
||||
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
|
||||
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
|
||||
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
|
||||
|
@ -434,6 +439,7 @@ setup-test-env:
|
|||
paths:
|
||||
- tmp/tests
|
||||
- config/secrets.yml
|
||||
- vendor/gitaly-ruby
|
||||
|
||||
rspec-pg 0 28: *rspec-metadata-pg
|
||||
rspec-pg 1 28: *rspec-metadata-pg
|
||||
|
@ -571,7 +577,7 @@ static-analysis:
|
|||
script:
|
||||
- scripts/static-analysis
|
||||
cache:
|
||||
key: "ruby-2.3.6-with-yarn-and-rubocop"
|
||||
key: "ruby-2.3.7-debian-stretch-with-yarn-and-rubocop"
|
||||
paths:
|
||||
- vendor/ruby
|
||||
- .yarn-cache/
|
||||
|
@ -647,7 +653,7 @@ migration:path-mysql:
|
|||
<<: *use-mysql
|
||||
|
||||
.db-rollback: &db-rollback
|
||||
<<: *dedicated-no-docs-pull-cache-job
|
||||
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
|
||||
script:
|
||||
- bundle exec rake db:migrate VERSION=20170523121229
|
||||
- bundle exec rake db:migrate
|
||||
|
@ -670,7 +676,7 @@ gitlab:setup-mysql:
|
|||
|
||||
# Frontend-related jobs
|
||||
gitlab:assets:compile:
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
|
||||
dependencies: []
|
||||
variables:
|
||||
NODE_ENV: "production"
|
||||
|
@ -691,7 +697,7 @@ gitlab:assets:compile:
|
|||
- webpack-report/
|
||||
|
||||
karma:
|
||||
<<: *dedicated-no-docs-pull-cache-job
|
||||
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
|
||||
<<: *use-pg
|
||||
dependencies:
|
||||
- compile-assets
|
||||
|
@ -720,7 +726,7 @@ codequality:
|
|||
tags: []
|
||||
before_script: []
|
||||
services:
|
||||
- docker:dind
|
||||
- docker:stable-dind
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
DOCKER_DRIVER: overlay2
|
||||
|
@ -735,16 +741,50 @@ codequality:
|
|||
expire_in: 1 week
|
||||
|
||||
sast:
|
||||
<<: *except-docs
|
||||
image: registry.gitlab.com/gitlab-org/gl-sast:latest
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
image: docker:stable
|
||||
variables:
|
||||
CONFIDENCE_LEVEL: 2
|
||||
SAST_CONFIDENCE_LEVEL: 2
|
||||
DOCKER_DRIVER: overlay2
|
||||
allow_failure: true
|
||||
tags: []
|
||||
before_script: []
|
||||
cache: {}
|
||||
dependencies: []
|
||||
services:
|
||||
- docker:stable-dind
|
||||
script:
|
||||
- /app/bin/run .
|
||||
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
|
||||
- docker run
|
||||
--env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}"
|
||||
--volume "$PWD:/code"
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock
|
||||
"registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
|
||||
artifacts:
|
||||
paths: [gl-sast-report.json]
|
||||
|
||||
dependency_scanning:
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
image: docker:stable
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
allow_failure: true
|
||||
tags: []
|
||||
before_script: []
|
||||
cache: {}
|
||||
dependencies: []
|
||||
services:
|
||||
- docker:stable-dind
|
||||
script:
|
||||
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
|
||||
- docker run
|
||||
--env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}"
|
||||
--volume "$PWD:/code"
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock
|
||||
"registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
|
||||
artifacts:
|
||||
paths: [gl-dependency-scanning-report.json]
|
||||
|
||||
qa:internal:
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
services: []
|
||||
|
@ -781,7 +821,7 @@ coverage:
|
|||
- coverage/assets/
|
||||
|
||||
lint:javascript:report:
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
|
||||
stage: post-test
|
||||
dependencies:
|
||||
- compile-assets
|
||||
|
|
70
.gitlab/issue_templates/Security Developer Workflow.md
Normal file
70
.gitlab/issue_templates/Security Developer Workflow.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
<!--
|
||||
# Read me first!
|
||||
|
||||
Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
|
||||
|
||||
Set the title to: `[Security] Description of the original issue`
|
||||
-->
|
||||
|
||||
### Prior to the security release
|
||||
|
||||
- [ ] 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**.
|
||||
- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
|
||||
|
||||
#### Backports
|
||||
|
||||
- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
|
||||
- [ ] 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 [seckpick 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
|
||||
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
|
||||
|
||||
[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
|
||||
|
||||
#### Documentation and final details
|
||||
|
||||
- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
|
||||
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
|
||||
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
|
||||
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
|
||||
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
|
||||
|
||||
### Summary
|
||||
#### Links
|
||||
|
||||
| Description | Link |
|
||||
| -------- | -------- |
|
||||
| Original issue | #TODO |
|
||||
| Security release issue | #TODO |
|
||||
| `master` MR | !TODO |
|
||||
| `master` MR (EE) | !TODO |
|
||||
| `Backport X.Y` MR | !TODO |
|
||||
| `Backport X.Y` MR | !TODO |
|
||||
| `Backport X.Y` MR | !TODO |
|
||||
| `Backport X.Y` MR (EE) | !TODO |
|
||||
| `Backport X.Y` MR (EE) | !TODO |
|
||||
| `Backport X.Y` MR (EE) | !TODO |
|
||||
|
||||
#### Details
|
||||
|
||||
| Description | Details | Further details|
|
||||
| -------- | -------- | -------- |
|
||||
| Versions affected | X.Y | |
|
||||
| Upgrade notes | | |
|
||||
| GitLab Settings updated | Yes/No| |
|
||||
| Migration required | Yes/No | |
|
||||
| Thanks | | |
|
||||
|
||||
[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
|
||||
[RM list]: https://about.gitlab.com/release-managers/
|
||||
|
||||
/label ~security
|
|
@ -45,4 +45,4 @@ When removing columns, tables, indexes or other structures:
|
|||
- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
|
||||
- [ ] Internationalization required/considered
|
||||
- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
|
||||
- [ ] End-to-end tests pass (`package-qa` manual pipeline job)
|
||||
- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)
|
||||
|
|
|
@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
|
|||
Lint/NestedPercentLiteral:
|
||||
Exclude:
|
||||
- 'lib/gitlab/git/repository.rb'
|
||||
- 'spec/support/email_format_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/email_format_shared_examples.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Lint/ReturnInVoidContext:
|
||||
|
@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
|
|||
- 'spec/lib/gitlab/diff/parser_spec.rb'
|
||||
- 'spec/lib/json_web_token/rsa_token_spec.rb'
|
||||
- 'spec/models/commit_spec.rb'
|
||||
- 'spec/support/repo_helpers.rb'
|
||||
- 'spec/support/seed_repo.rb'
|
||||
- 'spec/support/helpers/repo_helpers.rb'
|
||||
- 'spec/support/helpers/seed_repo.rb'
|
||||
|
||||
# Offense count: 112
|
||||
# Configuration parameters: Blacklist.
|
||||
|
@ -496,7 +496,7 @@ Style/EmptyLiteral:
|
|||
- 'spec/lib/gitlab/request_context_spec.rb'
|
||||
- 'spec/lib/gitlab/workhorse_spec.rb'
|
||||
- 'spec/requests/api/jobs_spec.rb'
|
||||
- 'spec/support/chat_slash_commands_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
|
||||
|
||||
# Offense count: 102
|
||||
# Cop supports --auto-correct.
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.3.6
|
||||
2.3.7
|
||||
|
|
268
CHANGELOG.md
268
CHANGELOG.md
|
@ -2,30 +2,58 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 10.7.7 (2018-07-17)
|
||||
## 10.8.7 (2018-07-26)
|
||||
|
||||
### Security (1 change)
|
||||
### Security (4 changes)
|
||||
|
||||
- Don't expose project names in various counters.
|
||||
- Don't expose project names in GitHub counters.
|
||||
- Adding CSRF protection to Hooks test action.
|
||||
- Fixed XSS in branch name in Web IDE.
|
||||
|
||||
### Fixed (1 change)
|
||||
|
||||
- Escapes milestone and label's names on flash notice when promoting them.
|
||||
|
||||
|
||||
## 10.8.6 (2018-07-17)
|
||||
|
||||
### Security (2 changes)
|
||||
|
||||
- Fix symlink vulnerability in project import.
|
||||
- Merge branch 'fix-mr-widget-border' into 'master'.
|
||||
|
||||
|
||||
## 10.7.6 (2018-06-21)
|
||||
## 10.8.5 (2018-06-21)
|
||||
|
||||
### Security (6 changes)
|
||||
### Security (5 changes)
|
||||
|
||||
- Fix XSS vulnerability for table of content generation.
|
||||
- Update sanitize gem to 4.6.5 to fix HTML injection vulnerability.
|
||||
- HTML escape branch name in project graphs page.
|
||||
- HTML escape the name of the user in ProjectsHelper#link_to_member.
|
||||
- Don't show events from internal projects for anonymous users in public feed.
|
||||
- XSS fix to use safe_params instead of params in url_for helpers.
|
||||
|
||||
### Other (1 change)
|
||||
|
||||
- Replacing gollum libraries for gitlab custom libs. !18343
|
||||
|
||||
|
||||
## 10.7.5 (2018-05-28)
|
||||
## 10.8.4 (2018-06-06)
|
||||
|
||||
- No changes.
|
||||
|
||||
## 10.8.3 (2018-05-30)
|
||||
|
||||
### Fixed (4 changes)
|
||||
|
||||
- Replace Gitlab::REVISION with Gitlab.revision and handle installations without a .git directory. !19125
|
||||
- Fix encoding of branch names on compare and new merge request page. !19143
|
||||
- Fix remote mirror database inconsistencies when upgrading from EE to CE. !19196
|
||||
- Fix local storage not being cleared after creating a new issue.
|
||||
|
||||
### Performance (1 change)
|
||||
|
||||
- Memoize Gitlab::Database.version.
|
||||
|
||||
|
||||
## 10.8.2 (2018-05-28)
|
||||
|
||||
### Security (3 changes)
|
||||
|
||||
|
@ -34,11 +62,195 @@ entry.
|
|||
- Fixed bug that allowed importing arbitrary project attributes.
|
||||
|
||||
|
||||
## 10.7.4 (2018-05-21)
|
||||
## 10.8.1 (2018-05-23)
|
||||
|
||||
### Fixed (1 change)
|
||||
### Fixed (9 changes)
|
||||
|
||||
- Allow CommitStatus class to use presentable methods. !18979
|
||||
- Fix corrupted environment pages with unathorized proxy url. !18989
|
||||
- Fixes deploy token variables on Ci::Build. !19047
|
||||
- Fix project mirror database inconsistencies when upgrading from EE to CE. !19109
|
||||
- Render 404 when prometheus adapter is disabled in Prometheus metrics controller. !19110
|
||||
- Fix error when deleting an empty list of refs.
|
||||
- Fixed U2F login when used with LDAP.
|
||||
- Bump prometheus-client-mmap to 0.9.3 to fix nil exception error.
|
||||
- Fix system hook not firing for blocked users when LDAP sign-in is used.
|
||||
|
||||
|
||||
## 10.8.0 (2018-05-22)
|
||||
|
||||
### Security (3 changes, 1 of them is from the community)
|
||||
|
||||
- Update faraday_middlewar to 0.12.2. !18397 (Takuya Noguchi)
|
||||
- Serve archive requests with the correct file in all cases.
|
||||
- Sanitizes user name to avoid XSS attacks.
|
||||
|
||||
### Fixed (47 changes, 11 of them are from the community)
|
||||
|
||||
- Refactor CSS to eliminate vertical misalignment of login nav. !16275 (Takuya Noguchi)
|
||||
- Fix pipeline status in branch/tag tree page. !17995
|
||||
- Allow group owner to enable runners from subgroups (#41981). !18009
|
||||
- Fix template selector menu visibility when toggling preview mode in file edit view. !18118 (Fabian Schneider)
|
||||
- Fix confirmation modal for deleting a protected branch. !18176 (Paul Bonaud @PaulRbR)
|
||||
- Triggering custom hooks by Wiki UI edit. !18251
|
||||
- Now `rake cache:clear` will also clear pipeline status cache. !18257
|
||||
- Fix `joined` information on project members page. !18290 (Fabian Schneider)
|
||||
- Fix missing namespace for some internal users. !18357
|
||||
- Show shared projects on group page. !18390
|
||||
- Restore label underline color. !18407 (George Tsiolis)
|
||||
- Fix undefined `html_escape` method during markdown rendering. !18418
|
||||
- Fix unassign slash command preview. !18447
|
||||
- Correct text and functionality for delete user / delete user and contributions modal. !18463 (Marc Schwede)
|
||||
- Fix discussions API setting created_at for notable in a group or notable in a project in a group with owners. !18464
|
||||
- Don't include lfs_file_locks data in export bundle. !18495
|
||||
- Reset milestone filter when clicking "Any Milestone" in dashboard. !18531
|
||||
- Ensure member notifications are sent after the member actual creation/update in the DB. !18538
|
||||
- Update links to /ci/lint with ones to project ci/lint. !18539 (Takuya Noguchi)
|
||||
- Fix tabs container styles to make RSS button clickable. !18559
|
||||
- Raise NoRepository error for non-valid repositories when calculating repository checksum. !18594
|
||||
- Don't automatically remove artifacts for pages jobs after pages:deploy has run. !18628
|
||||
- Increase new issue metadata form margin. !18630 (George Tsiolis)
|
||||
- Add loading icon padding for pipeline environments. !18631 (George Tsiolis)
|
||||
- ShaAttribute no longer stops startup if database is missing. !18726
|
||||
- Fix close keyboard shortcuts dialog using the keyboard shortcut. !18783 (Lars Greiss)
|
||||
- Fixes database inconsistencies between Community and Enterprise Edition on import state. !18811
|
||||
- Add database foreign key constraint between pipelines and build. !18822
|
||||
- Fix finding wiki pages when they have invalidly-encoded content. !18856
|
||||
- Fix outdated Web IDE welcome copy. !18861
|
||||
- fixed copy to blipboard button in embed bar of snippets. !18923 (haseebeqx)
|
||||
- Disables RBAC on nginx-ingress. !18947
|
||||
- Correct skewed Kubernetes popover illustration. !18949
|
||||
- Resolve Import/Export ci_cd_settings error updating the project. !46049
|
||||
- Fix project creation for user endpoint when jobs_enabled parameter supplied.
|
||||
- 46210 Display logo and user dropdown on mobile for terms page and fix styling.
|
||||
- Adds illustration for when job log was erased.
|
||||
- Ensure web hook 'blocked URL' errors are stored in web hook logs and properly surfaced to the user.
|
||||
- Make toggle markdown preview shortcut only toggle selected field.
|
||||
- Verifiy if pipeline has commit idetails and render information in MR widget when branch is deleted.
|
||||
- Fixed inconsistent protected branch pill baseline.
|
||||
- Fix setting Gitlab metrics content types.
|
||||
- Display only generic message on merge error to avoid exposing any potentially sensitive or user unfriendly backend messages.
|
||||
- Fix label links update on project transfer.
|
||||
- Breaks commit not found message in pipelines table.
|
||||
- Adjust issue boards list header label text color.
|
||||
- Prevent pipeline actions in dropdown to redirct to a new page.
|
||||
|
||||
### Changed (35 changes, 15 of them are from the community)
|
||||
|
||||
- Improve tooltips in collapsed right sidebar. !17714
|
||||
- Partition job_queue_duration_seconds with jobs_running_for_project. !17730
|
||||
- For group dashboard, we no longer show groups which the visitor is not a member of (this applies to admins and auditors). !17884 (Roger Rüttimann)
|
||||
- Use RFC 3676 mail signature delimiters. !17979 (Enrico Scholz)
|
||||
- Add sha filter to pipelines list API. !18125
|
||||
- New CI Job live-trace architecture. !18169
|
||||
- Make project deploy keys table more clearly structured. !18279
|
||||
- Remove green background from unlock button in admin area. !18288
|
||||
- Renamed Overview to Project in the contextual navigation at a project level. !18295 (Constance Okoghenun)
|
||||
- Load branches on new merge request page asynchronously. !18315
|
||||
- Create settings section for autodevops. !18321
|
||||
- Add a comma to the time estimate system notes. !18326
|
||||
- Enable specifying variables when executing a manual pipeline. !18440
|
||||
- Fix size and position for fork icon. !18449 (George Tsiolis)
|
||||
- Refactored activity calendar. !18469 (Enrico Scholz)
|
||||
- Small improvements to repository checks. !18484
|
||||
- Add 2FA filter to users API for admins only. !18503
|
||||
- Align project avatar on small viewports. !18513 (George Tsiolis)
|
||||
- Show group and project LFS settings in the interface to Owners and Masters. !18562
|
||||
- Update environment item action buttons icons. !18632 (George Tsiolis)
|
||||
- Update timeline icon for description edit. !18633 (George Tsiolis)
|
||||
- Revert discussion counter height. !18656 (George Tsiolis)
|
||||
- Improve quick actions summary preview. !18659 (George Tsiolis)
|
||||
- Change font for tables inside diff discussions. !18660 (George Tsiolis)
|
||||
- Add padding to profile description. !18663 (George Tsiolis)
|
||||
- Break issue title for board card title and issuable header text. !18674 (George Tsiolis)
|
||||
- Adds push mirrors to GitLab Community Edition. !18715
|
||||
- Inform the user when there are no project import options available. !18716 (George Tsiolis)
|
||||
- Improve commit message body rendering and fix responsive compare panels. !18725 (Constance Okoghenun)
|
||||
- Reconcile project templates with Auto DevOps. !18737
|
||||
- Remove branch name from the status bar of WebIDE.
|
||||
- Clean up WebIDE status bar and add useful info.
|
||||
- Improve interaction on WebIDE commit panel.
|
||||
- Keep current labels visible when editing them in the sidebar.
|
||||
- Use VueJS for rendering pipeline stages.
|
||||
|
||||
### Performance (26 changes, 11 of them are from the community)
|
||||
|
||||
- Move WorkInProgress vue component. !17536 (George Tsiolis)
|
||||
- Move ReadyToMerge vue component. !17545 (George Tsiolis)
|
||||
- Move BoardBlankState vue component. !17666 (George Tsiolis)
|
||||
- Improve DB performance of calculating total artifacts size. !17839
|
||||
- Add i18n and update specs for UnresolvedDiscussions vue component. !17866 (George Tsiolis)
|
||||
- Introduce new ProjectCiCdSetting model with group_runners_enabled. !18144
|
||||
- Move PipelineFailed vue component. !18277 (George Tsiolis)
|
||||
- Move TimeTrackingEstimateOnlyPane vue component. !18318 (George Tsiolis)
|
||||
- Move TimeTrackingHelpState vue component. !18319 (George Tsiolis)
|
||||
- Reduce queries on merge requests list page for merge requests from forks. !18561
|
||||
- Destroy build_chunks efficiently with FastDestroyAll module. !18575
|
||||
- Improve performance of a service responsible for creating a pipeline. !18582
|
||||
- Replace time_ago_in_words with JS-based one. !18607 (Takuya Noguchi)
|
||||
- Move TimeTrackingNoTrackingPane vue component. !18676 (George Tsiolis)
|
||||
- Move SidebarTimeTracking vue component. !18677 (George Tsiolis)
|
||||
- Move TimeTrackingSpentOnlyPane vue component. !18710 (George Tsiolis)
|
||||
- Detecting tags containing a commit uses Gitaly by default.
|
||||
- Increase cluster applications installer availability using alpine linux mirrors.
|
||||
- Compute notification recipients in background jobs.
|
||||
- Use persisted diff data instead fetching Git on discussions.
|
||||
- Detecting branchnames containing a commit uses Gitaly by default.
|
||||
- Detect repository license on Gitaly by default.
|
||||
- Finish NamespaceService migration to Gitaly.
|
||||
- Check if a ref exists is done by Gitaly by default.
|
||||
- Compute Gitlab::Git::Repository#checksum on Gitaly by default.
|
||||
- Repository#exists? is always executed through Gitaly.
|
||||
|
||||
### Added (22 changes, 10 of them are from the community)
|
||||
|
||||
- Allow group masters to configure runners for groups. !9646 (Alexis Reigel)
|
||||
- Adds Embedded Snippets Support. !15695 (haseebeqx)
|
||||
- Add Copy metadata quick action. !16473 (Mateusz Bajorski)
|
||||
- Show Runner's description on job's page. !17321
|
||||
- Add deprecation message to dynamic milestone pages. !17505
|
||||
- Show new branch/mr button even when branch exists. !17712 (Jacopo Beschi @jacopo-beschi)
|
||||
- API: add languages of project GET /projects/:id/languages. !17770 (Roger Rüttimann)
|
||||
- Display active sessions and allow the user to revoke any of it. !17867 (Alexis Reigel)
|
||||
- Add cron job to email users on issue due date. !17985 (Stuart Nelson)
|
||||
- Rubocop rule to avoid returning from a block. !18000 (Jacopo Beschi @jacopo-beschi)
|
||||
- Add the signature verfication badge to the compare view. !18245 (Marc Shaw)
|
||||
- Expose Deploy Token data as environment varialbes on CI/CD jobs. !18414
|
||||
- Show group id in group settings. !18482 (George Tsiolis)
|
||||
- Allow admins to enforce accepting Terms of Service on an instance. !18570
|
||||
- Add CI_COMMIT_MESSAGE, CI_COMMIT_TITLE and CI_COMMIT_DESCRIPTION predefined variables. !18672
|
||||
- Add GCP signup offer to cluster index / create pages. !18684
|
||||
- Output some useful information when running the rails console. !18697
|
||||
- Display merge commit SHA in merge widget after merge. !18722
|
||||
- git SHA is now displayed alongside the GitLab version on the Admin Dashboard.
|
||||
- Expose the target commit ID through the tag API.
|
||||
- Added fuzzy file finder to web IDE.
|
||||
- Add discussion API for merge requests and commits.
|
||||
|
||||
### Other (22 changes, 8 of them are from the community)
|
||||
|
||||
- Replace the `project/issues/milestones.feature` spinach test with an rspec analog. !18300 (@blackst0ne)
|
||||
- Replace the `project/commits/branches.feature` spinach test with an rspec analog. !18302 (@blackst0ne)
|
||||
- Replacing gollum libraries for gitlab custom libs. !18343
|
||||
- Replace the `project/commits/comments.feature` spinach test with an rspec analog. !18356 (@blackst0ne)
|
||||
- Replace "Click" with "Select" to be more inclusive of people with accessibility requirements. !18386 (Mark Lapierre)
|
||||
- Remove ahead/behind graphs on project branches on mobile. !18415 (Takuya Noguchi)
|
||||
- Replace the `project/source/markdown_render.feature` spinach test with an rspec analog. !18525 (@blackst0ne)
|
||||
- Add missing changelog type to docs. !18526 (@blackst0ne)
|
||||
- Added Webhook SSRF prevention to documentation. !18532
|
||||
- Upgrade underscore.js to 1.9.0. !18578
|
||||
- Add documentation about how to use variables to define deploy policies for staging/production environments. !18675
|
||||
- Replace the `project/builds/artifacts.feature` spinach test with an rspec analog. !18729 (@blackst0ne)
|
||||
- Block access to the API & git for users that did not accept enforced Terms of Service. !18816
|
||||
- Transition to atomic internal ids for all models. !44259
|
||||
- Removes modal boards store and mixins from global scope.
|
||||
- Replace GKE acronym with Google Kubernetes Engine.
|
||||
- Replace vue resource with axios for pipelines details page.
|
||||
- Enable prometheus monitoring by default.
|
||||
- Replace vue resource with axios in pipelines table.
|
||||
- Bump lograge to 0.10.0 and remove monkey patch.
|
||||
- Improves wording in new pipeline page.
|
||||
- Gitaly handles repository forks by default.
|
||||
|
||||
|
||||
## 10.7.3 (2018-05-02)
|
||||
|
@ -298,6 +510,31 @@ entry.
|
|||
- Upgrade Gitaly to upgrade its charlock_holmes.
|
||||
|
||||
|
||||
## 10.6.5 (2018-04-24)
|
||||
|
||||
### Security (1 change)
|
||||
|
||||
- Sanitizes user name to avoid XSS attacks.
|
||||
|
||||
|
||||
## 10.6.4 (2018-04-09)
|
||||
|
||||
### Fixed (8 changes, 1 of them is from the community)
|
||||
|
||||
- Correct copy text for the promote milestone and label modals. !17726
|
||||
- Avoid validation errors when running the Pages domain verification service. !17992
|
||||
- Fix autolinking URLs containing ampersands. !18045
|
||||
- Fix exceptions raised when migrating pipeline stages in the background. !18076
|
||||
- Work around Prometheus Helm chart name changes to fix integration. !18206 (joshlambert)
|
||||
- Don't show Jump to Discussion button on Issues.
|
||||
- Fix listing commit branch/tags that contain special characters.
|
||||
- Fix 404 in group boards when moving issue between lists.
|
||||
|
||||
### Performance (1 change)
|
||||
|
||||
- Free open file descriptors and libgit2 buffers in UpdatePagesService.
|
||||
|
||||
|
||||
## 10.6.3 (2018-04-03)
|
||||
|
||||
### Security (2 changes)
|
||||
|
@ -521,6 +758,13 @@ entry.
|
|||
- Use host URL to build JIRA remote link icon.
|
||||
|
||||
|
||||
## 10.5.8 (2018-04-24)
|
||||
|
||||
### Security (1 change)
|
||||
|
||||
- Sanitizes user name to avoid XSS attacks.
|
||||
|
||||
|
||||
## 10.5.7 (2018-04-03)
|
||||
|
||||
### Security (2 changes)
|
||||
|
|
|
@ -9,6 +9,10 @@ terms.
|
|||
|
||||
[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
|
||||
|
||||
All Documentation content that resides under the [doc/ directory](/doc) of this
|
||||
repository is licensed under Creative Commons:
|
||||
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
_This notice should stay as the first item in the CONTRIBUTING.md file._
|
||||
|
||||
---
|
||||
|
@ -25,10 +29,12 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
|
|||
- [Workflow labels](#workflow-labels)
|
||||
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
|
||||
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
|
||||
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
|
||||
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release)
|
||||
- [Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc)
|
||||
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
|
||||
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
|
||||
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
|
||||
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
|
||||
- [Implement design & UI elements](#implement-design-ui-elements)
|
||||
- [Implement design & UI elements](#implement-design--ui-elements)
|
||||
- [Issue tracker](#issue-tracker)
|
||||
- [Issue triaging](#issue-triaging)
|
||||
- [Feature proposals](#feature-proposals)
|
||||
|
@ -125,8 +131,10 @@ Most issues will have labels for at least one of the following:
|
|||
|
||||
- Type: ~"feature proposal", ~bug, ~customer, etc.
|
||||
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
|
||||
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
|
||||
- Priority: ~Deliverable, ~Stretch, ~"Next Patch Release"
|
||||
- Team: ~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.
|
||||
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
|
||||
- Priority: ~P1, ~P2, ~P3, ~P4
|
||||
- Severity: ~S1, ~S2, ~S3, ~S4
|
||||
|
||||
All labels, their meaning and priority are defined on the
|
||||
[labels page][labels-page].
|
||||
|
@ -167,13 +175,13 @@ Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
|
|||
|
||||
Subject labels are always all-lowercase.
|
||||
|
||||
### Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)
|
||||
### Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)
|
||||
|
||||
Team labels specify what team is responsible for this issue.
|
||||
Assigning a team label makes sure issues get the attention of the appropriate
|
||||
people.
|
||||
|
||||
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
|
||||
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
|
||||
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
|
||||
|
||||
The descriptions on the [labels page][labels-page] explain what falls under the
|
||||
|
@ -185,10 +193,10 @@ indicate if an issue needs backend work, frontend work, or both.
|
|||
Team labels are always capitalized so that they show up as the first label for
|
||||
any issue.
|
||||
|
||||
### Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")
|
||||
### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
|
||||
|
||||
Priority labels help us clearly communicate expectations of the work for the
|
||||
release. There are two levels of priority labels:
|
||||
Milestone labels help us clearly communicate expectations of the work for the
|
||||
release. There are three levels of Milestone labels:
|
||||
|
||||
- ~Deliverable: Issues that are expected to be delivered in the current
|
||||
milestone.
|
||||
|
@ -203,16 +211,46 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
|
|||
or ~"Stretch". Any open issue for a previous milestone should be labeled
|
||||
~"Next Patch Release", or otherwise rescheduled to a different milestone.
|
||||
|
||||
### Severity labels (~S1, ~S2, etc.)
|
||||
### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
|
||||
|
||||
Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be. If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
|
||||
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
|
||||
|
||||
| Label | Meaning | Estimate time to fix | Guidance |
|
||||
|-------|-----------------|------------------------------------------------------------------|----------|
|
||||
| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
|
||||
| ~P2 | High Priority | The next release | |
|
||||
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
|
||||
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
|
||||
|
||||
#### Specific Priority guidance
|
||||
|
||||
| Label | Availability / Performance |
|
||||
|-------|--------------------------------------------------------------|
|
||||
| ~P1 | |
|
||||
| ~P2 | The issue is (almost) guaranteed to occur in the near future |
|
||||
| ~P3 | The issue is likely to occur in the near future |
|
||||
| ~P4 | The issue _may_ occur but it's not likely |
|
||||
|
||||
### Bug Severity labels (~S1, ~S2, ~S3 & etc.)
|
||||
|
||||
Severity labels help us clearly communicate the impact of a ~bug on users.
|
||||
|
||||
| Label | Meaning | Example |
|
||||
|-------|------------------------------------------|---------|
|
||||
| ~S1 | Feature broken, no workaround | Unable to create an issue |
|
||||
| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
|
||||
| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
|
||||
| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
|
||||
| Label | Meaning | Impact of the defect | Example |
|
||||
|-------|-------------------|-------------------------------------------------------|---------|
|
||||
| ~S1 | Blocker | Outage, broken feature with no workaround | Unable to create an issue. Data corruption/loss. Security breach. |
|
||||
| ~S2 | Critical Severity | Broken Feature, workaround too complex & unacceptable | Can push commits, but only via the command line. |
|
||||
| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
|
||||
| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
|
||||
|
||||
#### Specific Severity guidance
|
||||
|
||||
| Label | Security Impact |
|
||||
|-------|-------------------------------------------------------------------|
|
||||
| ~S1 | >50% customers impacted (possible company extinction level event) |
|
||||
| ~S2 | Multiple customers impacted (but not apocalyptic) |
|
||||
| ~S3 | A single customer impacted |
|
||||
| ~S4 | No customer impact, or expected impact within 30 days |
|
||||
|
||||
### Label for community contributors (~"Accepting Merge Requests")
|
||||
|
||||
|
@ -693,4 +731,3 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
|
|||
|
||||
[^1]: Please note that specs other than JavaScript specs are considered backend
|
||||
code.
|
||||
|
|
@ -1 +1 @@
|
|||
0.96.1
|
||||
0.100.1
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.8.1
|
||||
0.9.1
|
||||
|
|
|
@ -1 +1 @@
|
|||
4.1.0
|
||||
4.2.1
|
||||
|
|
17
Gemfile
17
Gemfile
|
@ -33,7 +33,7 @@ gem 'grape-route-helpers', '~> 2.1.0'
|
|||
gem 'faraday', '~> 0.12'
|
||||
|
||||
# Authentication libraries
|
||||
gem 'devise', '~> 4.2'
|
||||
gem 'devise', '~> 4.4'
|
||||
gem 'doorkeeper', '~> 4.3'
|
||||
gem 'doorkeeper-openid_connect', '~> 1.3'
|
||||
gem 'omniauth', '~> 1.8'
|
||||
|
@ -41,7 +41,7 @@ gem 'omniauth-auth0', '~> 2.0.0'
|
|||
gem 'omniauth-azure-oauth2', '~> 0.0.9'
|
||||
gem 'omniauth-cas3', '~> 1.1.4'
|
||||
gem 'omniauth-facebook', '~> 4.0.0'
|
||||
gem 'omniauth-github', '~> 1.1.1'
|
||||
gem 'omniauth-github', '~> 1.3'
|
||||
gem 'omniauth-gitlab', '~> 1.0.2'
|
||||
gem 'omniauth-google-oauth2', '~> 0.5.3'
|
||||
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
|
||||
|
@ -81,7 +81,7 @@ gem 'net-ldap'
|
|||
|
||||
# Git Wiki
|
||||
# Required manually in config/initializers/gollum.rb to control load order
|
||||
gem 'gitlab-gollum-lib', '~> 4.2'
|
||||
gem 'gitlab-gollum-lib', '~> 4.2', require: false
|
||||
|
||||
gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
|
||||
|
||||
|
@ -90,7 +90,7 @@ gem 'github-linguist', '~> 5.3.3', require: 'linguist'
|
|||
|
||||
# API
|
||||
gem 'grape', '~> 1.0'
|
||||
gem 'grape-entity', '~> 0.6.0'
|
||||
gem 'grape-entity', '~> 0.7.1'
|
||||
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
|
||||
|
||||
# Disable strong_params so that Mash does not respond to :permitted?
|
||||
|
@ -184,6 +184,9 @@ gem 're2', '~> 1.1.1'
|
|||
|
||||
gem 'version_sorter', '~> 2.1.0'
|
||||
|
||||
# User agent parsing
|
||||
gem 'device_detector'
|
||||
|
||||
# Cache
|
||||
gem 'redis-rails', '~> 5.0.2'
|
||||
|
||||
|
@ -282,7 +285,6 @@ gem 'batch-loader', '~> 1.2.1'
|
|||
gem 'peek', '~> 1.0.1'
|
||||
gem 'peek-gc', '~> 0.0.2'
|
||||
gem 'peek-mysql2', '~> 1.1.0', group: :mysql
|
||||
gem 'peek-performance_bar', '~> 1.3.0'
|
||||
gem 'peek-pg', '~> 1.3.0', group: :postgres
|
||||
gem 'peek-rblineprof', '~> 0.2.0'
|
||||
gem 'peek-redis', '~> 1.2.0'
|
||||
|
@ -295,7 +297,7 @@ group :metrics do
|
|||
gem 'influxdb', '~> 0.2', require: false
|
||||
|
||||
# Prometheus
|
||||
gem 'prometheus-client-mmap', '~> 0.9.1'
|
||||
gem 'prometheus-client-mmap', '~> 0.9.3'
|
||||
gem 'raindrops', '~> 0.18'
|
||||
end
|
||||
|
||||
|
@ -376,6 +378,7 @@ group :test do
|
|||
gem 'email_spec', '~> 1.6.0'
|
||||
gem 'json-schema', '~> 2.8.0'
|
||||
gem 'webmock', '~> 2.3.2'
|
||||
gem 'rails-controller-testing' if rails5? # Rails5 only gem.
|
||||
gem 'test_after_commit', '~> 1.1' unless rails5? # Remove this gem when migrated to rails 5.0. It's been integrated to rails 5.0.
|
||||
gem 'sham_rack', '~> 1.3.6'
|
||||
gem 'concurrent-ruby', '~> 1.0.5'
|
||||
|
@ -413,7 +416,7 @@ group :ed25519 do
|
|||
end
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly-proto', '~> 0.96.0', require: 'gitaly'
|
||||
gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly'
|
||||
gem 'grpc', '~> 1.11.0'
|
||||
|
||||
# Locked until https://github.com/google/protobuf/issues/4210 is closed
|
||||
|
|
63
Gemfile.lock
63
Gemfile.lock
|
@ -143,7 +143,7 @@ GEM
|
|||
connection_pool (2.2.1)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.3)
|
||||
crass (1.0.4)
|
||||
creole (0.5.0)
|
||||
css_parser (1.5.0)
|
||||
addressable
|
||||
|
@ -161,10 +161,11 @@ GEM
|
|||
activerecord (>= 3.2.0, < 5.1)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
devise (4.2.0)
|
||||
device_detector (1.0.0)
|
||||
devise (4.4.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0, < 5.1)
|
||||
railties (>= 4.1.0, < 6.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (3.0.0)
|
||||
|
@ -206,7 +207,7 @@ GEM
|
|||
railties (>= 3.0.0)
|
||||
faraday (0.12.2)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday_middleware (0.11.0.1)
|
||||
faraday_middleware (0.12.2)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
faraday_middleware-multi_json (0.0.6)
|
||||
faraday_middleware
|
||||
|
@ -290,7 +291,7 @@ GEM
|
|||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly-proto (0.96.0)
|
||||
gitaly-proto (0.99.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.10)
|
||||
github-linguist (5.3.3)
|
||||
|
@ -365,8 +366,8 @@ GEM
|
|||
rack (>= 1.3.0)
|
||||
rack-accept
|
||||
virtus (>= 1.0.0)
|
||||
grape-entity (0.6.0)
|
||||
activesupport
|
||||
grape-entity (0.7.1)
|
||||
activesupport (>= 4.0)
|
||||
multi_json (>= 1.3.2)
|
||||
grape-route-helpers (2.1.0)
|
||||
activesupport
|
||||
|
@ -483,10 +484,11 @@ GEM
|
|||
logging (2.2.2)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.10)
|
||||
lograge (0.5.1)
|
||||
actionpack (>= 4, < 5.2)
|
||||
activesupport (>= 4, < 5.2)
|
||||
railties (>= 4, < 5.2)
|
||||
lograge (0.10.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.2.2)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
|
@ -546,9 +548,9 @@ GEM
|
|||
omniauth (~> 1.2)
|
||||
omniauth-facebook (4.0.0)
|
||||
omniauth-oauth2 (~> 1.2)
|
||||
omniauth-github (1.1.2)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-github (1.3.0)
|
||||
omniauth (~> 1.5)
|
||||
omniauth-oauth2 (>= 1.4.0, < 2.0)
|
||||
omniauth-gitlab (1.0.2)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (~> 1.0)
|
||||
|
@ -586,7 +588,7 @@ GEM
|
|||
orm_adapter (0.5.0)
|
||||
os (0.9.6)
|
||||
parallel (1.12.1)
|
||||
parser (2.5.0.5)
|
||||
parser (2.5.1.0)
|
||||
ast (~> 2.4.0)
|
||||
parslet (1.5.0)
|
||||
blankslate (~> 2.0)
|
||||
|
@ -601,8 +603,6 @@ GEM
|
|||
atomic (>= 1.0.0)
|
||||
mysql2
|
||||
peek
|
||||
peek-performance_bar (1.3.1)
|
||||
peek (>= 0.1.0)
|
||||
peek-pg (1.3.0)
|
||||
concurrent-ruby
|
||||
concurrent-ruby-ext
|
||||
|
@ -636,7 +636,7 @@ GEM
|
|||
parser
|
||||
unparser
|
||||
procto (0.0.3)
|
||||
prometheus-client-mmap (0.9.1)
|
||||
prometheus-client-mmap (0.9.3)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
|
@ -648,7 +648,7 @@ GEM
|
|||
pry (>= 0.9.10)
|
||||
public_suffix (3.0.2)
|
||||
pyu-ruby-sasl (0.0.3.3)
|
||||
rack (1.6.9)
|
||||
rack (1.6.10)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (4.4.1)
|
||||
|
@ -696,7 +696,7 @@ GEM
|
|||
rainbow (2.2.2)
|
||||
rake
|
||||
raindrops (0.18.0)
|
||||
rake (12.3.0)
|
||||
rake (12.3.1)
|
||||
rb-fsevent (0.10.2)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
|
@ -737,8 +737,9 @@ GEM
|
|||
declarative-option (< 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
request_store (1.3.1)
|
||||
responders (2.3.0)
|
||||
railties (>= 4.2.0, < 5.1)
|
||||
responders (2.4.0)
|
||||
actionpack (>= 4.2.0, < 5.3)
|
||||
railties (>= 4.2.0, < 5.3)
|
||||
rest-client (2.0.2)
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
|
@ -812,7 +813,7 @@ GEM
|
|||
rubyzip (1.2.1)
|
||||
rufus-scheduler (3.4.0)
|
||||
et-orbi (~> 1.0)
|
||||
rugged (0.27.0)
|
||||
rugged (0.27.1)
|
||||
safe_yaml (1.0.4)
|
||||
sanitize (4.6.5)
|
||||
crass (~> 1.0.2)
|
||||
|
@ -943,7 +944,7 @@ GEM
|
|||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.5)
|
||||
unicode-display_width (1.3.0)
|
||||
unicode-display_width (1.3.2)
|
||||
unicorn (5.1.0)
|
||||
kgio (~> 2.6)
|
||||
raindrops (~> 0.7)
|
||||
|
@ -970,7 +971,7 @@ GEM
|
|||
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||
equalizer (~> 0.0, >= 0.0.9)
|
||||
vmstat (2.3.0)
|
||||
warden (1.2.6)
|
||||
warden (1.2.7)
|
||||
rack (>= 1.0)
|
||||
webmock (2.3.2)
|
||||
addressable (>= 2.3.6)
|
||||
|
@ -1031,7 +1032,8 @@ DEPENDENCIES
|
|||
database_cleaner (~> 1.5.0)
|
||||
deckar01-task_list (= 2.0.0)
|
||||
default_value_for (~> 3.0.0)
|
||||
devise (~> 4.2)
|
||||
device_detector
|
||||
devise (~> 4.4)
|
||||
devise-two-factor (~> 3.0.0)
|
||||
diffy (~> 3.1.0)
|
||||
doorkeeper (~> 4.3)
|
||||
|
@ -1062,7 +1064,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 0.96.0)
|
||||
gitaly-proto (~> 0.99.0)
|
||||
github-linguist (~> 5.3.3)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-gollum-lib (~> 4.2)
|
||||
|
@ -1075,7 +1077,7 @@ DEPENDENCIES
|
|||
google-protobuf (= 3.5.1)
|
||||
gpgme
|
||||
grape (~> 1.0)
|
||||
grape-entity (~> 0.6.0)
|
||||
grape-entity (~> 0.7.1)
|
||||
grape-route-helpers (~> 2.1.0)
|
||||
grape_logging (~> 1.7)
|
||||
grpc (~> 1.11.0)
|
||||
|
@ -1116,7 +1118,7 @@ DEPENDENCIES
|
|||
omniauth-azure-oauth2 (~> 0.0.9)
|
||||
omniauth-cas3 (~> 1.1.4)
|
||||
omniauth-facebook (~> 4.0.0)
|
||||
omniauth-github (~> 1.1.1)
|
||||
omniauth-github (~> 1.3)
|
||||
omniauth-gitlab (~> 1.0.2)
|
||||
omniauth-google-oauth2 (~> 0.5.3)
|
||||
omniauth-kerberos (~> 0.3.0)
|
||||
|
@ -1129,14 +1131,13 @@ DEPENDENCIES
|
|||
peek (~> 1.0.1)
|
||||
peek-gc (~> 0.0.2)
|
||||
peek-mysql2 (~> 1.1.0)
|
||||
peek-performance_bar (~> 1.3.0)
|
||||
peek-pg (~> 1.3.0)
|
||||
peek-rblineprof (~> 0.2.0)
|
||||
peek-redis (~> 1.2.0)
|
||||
peek-sidekiq (~> 1.0.3)
|
||||
pg (~> 0.18.2)
|
||||
premailer-rails (~> 1.9.7)
|
||||
prometheus-client-mmap (~> 0.9.1)
|
||||
prometheus-client-mmap (~> 0.9.3)
|
||||
pry-byebug (~> 3.4.1)
|
||||
pry-rails (~> 0.3.4)
|
||||
rack-attack (~> 4.4.1)
|
||||
|
|
|
@ -69,7 +69,7 @@ GEM
|
|||
unf
|
||||
ast (2.4.0)
|
||||
atomic (1.1.100)
|
||||
attr_encrypted (3.0.3)
|
||||
attr_encrypted (3.1.0)
|
||||
encryptor (~> 3.0.0)
|
||||
attr_required (1.0.1)
|
||||
autoprefixer-rails (8.1.0.1)
|
||||
|
@ -162,6 +162,7 @@ GEM
|
|||
activerecord (>= 3.2.0, < 5.2)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
device_detector (1.0.1)
|
||||
devise (4.4.1)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
|
@ -291,9 +292,9 @@ GEM
|
|||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly-proto (0.94.0)
|
||||
gitaly-proto (0.97.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.0)
|
||||
grpc (~> 1.10)
|
||||
github-linguist (5.3.3)
|
||||
charlock_holmes (~> 0.7.5)
|
||||
escape_utils (~> 1.1.0)
|
||||
|
@ -304,6 +305,17 @@ GEM
|
|||
flowdock (~> 0.7)
|
||||
gitlab-grit (>= 2.4.1)
|
||||
multi_json
|
||||
gitlab-gollum-lib (4.2.7.2)
|
||||
gemojione (~> 3.2)
|
||||
github-markup (~> 1.6)
|
||||
gollum-grit_adapter (~> 1.0)
|
||||
nokogiri (>= 1.6.1, < 2.0)
|
||||
rouge (~> 3.1)
|
||||
sanitize (~> 2.1)
|
||||
stringex (~> 2.6)
|
||||
gitlab-gollum-rugged_adapter (0.4.4)
|
||||
mime-types (>= 1.15)
|
||||
rugged (~> 0.25)
|
||||
gitlab-grit (2.8.2)
|
||||
charlock_holmes (~> 0.6)
|
||||
diff-lcs (~> 1.1)
|
||||
|
@ -323,17 +335,6 @@ GEM
|
|||
activesupport (>= 4.2.0)
|
||||
gollum-grit_adapter (1.0.1)
|
||||
gitlab-grit (~> 2.7, >= 2.7.1)
|
||||
gollum-lib (4.2.7)
|
||||
gemojione (~> 3.2)
|
||||
github-markup (~> 1.6)
|
||||
gollum-grit_adapter (~> 1.0)
|
||||
nokogiri (>= 1.6.1, < 2.0)
|
||||
rouge (~> 2.1)
|
||||
sanitize (~> 2.1)
|
||||
stringex (~> 2.6)
|
||||
gollum-rugged_adapter (0.4.4)
|
||||
mime-types (>= 1.15)
|
||||
rugged (~> 0.25)
|
||||
gon (6.1.0)
|
||||
actionpack (>= 3.0)
|
||||
json
|
||||
|
@ -375,7 +376,7 @@ GEM
|
|||
rake
|
||||
grape_logging (1.7.0)
|
||||
grape
|
||||
grpc (1.10.0)
|
||||
grpc (1.11.0)
|
||||
google-protobuf (~> 3.1)
|
||||
googleapis-common-protos-types (~> 1.0.0)
|
||||
googleauth (>= 0.5.1, < 0.7)
|
||||
|
@ -554,9 +555,6 @@ GEM
|
|||
jwt (>= 1.5)
|
||||
omniauth (>= 1.1.1)
|
||||
omniauth-oauth2 (>= 1.5)
|
||||
omniauth-jwt (0.0.2)
|
||||
jwt
|
||||
omniauth (~> 1.1)
|
||||
omniauth-kerberos (0.3.0)
|
||||
omniauth-multipassword
|
||||
timfel-krb5-auth (~> 0.8)
|
||||
|
@ -602,8 +600,6 @@ GEM
|
|||
atomic (>= 1.0.0)
|
||||
mysql2
|
||||
peek
|
||||
peek-performance_bar (1.3.1)
|
||||
peek (>= 0.1.0)
|
||||
peek-pg (1.3.0)
|
||||
concurrent-ruby
|
||||
concurrent-ruby-ext
|
||||
|
@ -678,6 +674,10 @@ GEM
|
|||
bundler (>= 1.3.0)
|
||||
railties (= 5.0.6)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.2)
|
||||
actionpack (~> 5.x, >= 5.0.1)
|
||||
actionview (~> 5.x, >= 5.0.1)
|
||||
activesupport (~> 5.x)
|
||||
rails-deprecated_sanitizer (1.0.3)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
rails-dom-testing (2.0.3)
|
||||
|
@ -748,7 +748,7 @@ GEM
|
|||
retriable (3.1.1)
|
||||
rinku (2.0.4)
|
||||
rotp (2.1.2)
|
||||
rouge (2.2.1)
|
||||
rouge (3.1.1)
|
||||
rqrcode (0.10.1)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode-rails3 (0.1.7)
|
||||
|
@ -874,7 +874,7 @@ GEM
|
|||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-notifier (1.5.1)
|
||||
spinach (0.10.1)
|
||||
spinach (0.8.10)
|
||||
colorize
|
||||
gherkin-ruby (>= 0.3.2)
|
||||
json
|
||||
|
@ -1002,7 +1002,7 @@ DEPENDENCIES
|
|||
asciidoctor (~> 1.5.6)
|
||||
asciidoctor-plantuml (= 0.0.8)
|
||||
asset_sync (~> 2.2.0)
|
||||
attr_encrypted (~> 3.0.0)
|
||||
attr_encrypted (~> 3.1.0)
|
||||
awesome_print (~> 1.2.0)
|
||||
babosa (~> 1.0.2)
|
||||
base32 (~> 0.3.0)
|
||||
|
@ -1031,6 +1031,7 @@ DEPENDENCIES
|
|||
database_cleaner (~> 1.5.0)
|
||||
deckar01-task_list (= 2.0.0)
|
||||
default_value_for (~> 3.0.5)
|
||||
device_detector
|
||||
devise (~> 4.2)
|
||||
devise-two-factor (~> 3.0.0)
|
||||
diffy (~> 3.1.0)
|
||||
|
@ -1062,14 +1063,14 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 0.94.0)
|
||||
gitaly-proto (~> 0.97.0)
|
||||
github-linguist (~> 5.3.3)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-gollum-lib (~> 4.2)
|
||||
gitlab-gollum-rugged_adapter (~> 0.4.4)
|
||||
gitlab-markup (~> 1.6.2)
|
||||
gitlab-styles (~> 2.3)
|
||||
gitlab_omniauth-ldap (~> 2.0.4)
|
||||
gollum-lib (~> 4.2)
|
||||
gollum-rugged_adapter (~> 0.4.4)
|
||||
gon (~> 6.1.0)
|
||||
google-api-client (~> 0.19.8)
|
||||
google-protobuf (= 3.5.1)
|
||||
|
@ -1078,7 +1079,7 @@ DEPENDENCIES
|
|||
grape-entity (~> 0.6.0)
|
||||
grape-route-helpers (~> 2.1.0)
|
||||
grape_logging (~> 1.7)
|
||||
grpc (~> 1.10.0)
|
||||
grpc (~> 1.11.0)
|
||||
haml_lint (~> 0.26.0)
|
||||
hamlit (~> 2.6.1)
|
||||
hashie-forbidden_attributes
|
||||
|
@ -1119,7 +1120,6 @@ DEPENDENCIES
|
|||
omniauth-github (~> 1.1.1)
|
||||
omniauth-gitlab (~> 1.0.2)
|
||||
omniauth-google-oauth2 (~> 0.5.3)
|
||||
omniauth-jwt (~> 0.0.2)
|
||||
omniauth-kerberos (~> 0.3.0)
|
||||
omniauth-oauth2-generic (~> 0.2.2)
|
||||
omniauth-saml (~> 1.10)
|
||||
|
@ -1130,7 +1130,6 @@ DEPENDENCIES
|
|||
peek (~> 1.0.1)
|
||||
peek-gc (~> 0.0.2)
|
||||
peek-mysql2 (~> 1.1.0)
|
||||
peek-performance_bar (~> 1.3.0)
|
||||
peek-pg (~> 1.3.0)
|
||||
peek-rblineprof (~> 0.2.0)
|
||||
peek-redis (~> 1.2.0)
|
||||
|
@ -1145,6 +1144,7 @@ DEPENDENCIES
|
|||
rack-oauth2 (~> 1.2.1)
|
||||
rack-proxy (~> 0.6.0)
|
||||
rails (= 5.0.6)
|
||||
rails-controller-testing
|
||||
rails-deprecated_sanitizer (~> 1.0.3)
|
||||
rails-i18n (~> 5.1)
|
||||
rainbow (~> 2.2)
|
||||
|
@ -1161,7 +1161,7 @@ DEPENDENCIES
|
|||
redis-rails (~> 5.0.2)
|
||||
request_store (~> 1.3)
|
||||
responders (~> 2.0)
|
||||
rouge (~> 2.0)
|
||||
rouge (~> 3.1)
|
||||
rqrcode-rails3 (~> 0.1.7)
|
||||
rspec-parameterized
|
||||
rspec-rails (~> 3.6.0)
|
||||
|
|
27
LICENSE
27
LICENSE
|
@ -1,25 +1,12 @@
|
|||
Copyright (c) 2011-2017 GitLab B.V.
|
||||
Copyright GitLab B.V.
|
||||
|
||||
With regard to the GitLab Software:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
---
|
||||
|
||||
For all third party components incorporated into the GitLab Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
All Documentation content that resides under the doc/ directory of this
|
||||
repository is licensed under Creative Commons: CC BY-SA 4.0.
|
||||
|
|
|
@ -67,6 +67,12 @@ You can access a new installation with the login **`root`** and password **`5ive
|
|||
|
||||
GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
|
||||
|
||||
## Licensing
|
||||
|
||||
GitLab Community Edition (CE) is available freely under the MIT Expat license.
|
||||
|
||||
All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
|
||||
|
||||
## Install a development environment
|
||||
|
||||
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
10.7.7
|
||||
10.8.7
|
||||
|
|
BIN
app/assets/images/ext_snippet_icons/ext_snippet_icons.png
Normal file
BIN
app/assets/images/ext_snippet_icons/ext_snippet_icons.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1,018 B |
BIN
app/assets/images/ext_snippet_icons/logo.png
Normal file
BIN
app/assets/images/ext_snippet_icons/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 494 B |
|
@ -7,27 +7,24 @@ 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;
|
||||
const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
|
||||
|
||||
const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
|
||||
this.childNodes,
|
||||
childNode => childNode.nodeType === 3,
|
||||
);
|
||||
const isEmojiUnicode =
|
||||
this.childNodes &&
|
||||
Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
|
||||
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
|
||||
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
|
||||
|
||||
if (
|
||||
emojiUnicode &&
|
||||
isEmojiUnicode &&
|
||||
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
|
||||
) {
|
||||
if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
|
||||
// CSS sprite fallback takes precedence over image fallback
|
||||
if (hasCssSpriteFalback) {
|
||||
if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
|
||||
const emojiSpriteLinkTag = document.createElement('link');
|
||||
emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
|
||||
emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
|
||||
document.head.appendChild(emojiSpriteLinkTag);
|
||||
gon.emoji_sprites_css_added = true;
|
||||
}
|
||||
// IE 11 doesn't like adding multiple at once :(
|
||||
this.classList.add('emoji-icon');
|
||||
this.classList.add(fallbackSpriteClass);
|
||||
|
|
|
@ -94,7 +94,7 @@ export default class FileTemplateMediator {
|
|||
const hash = urlPieces[1];
|
||||
if (hash === 'preview') {
|
||||
this.hideTemplateSelectorMenu();
|
||||
} else if (hash === 'editor') {
|
||||
} else if (hash === 'editor' && !this.typeSelector.isHidden()) {
|
||||
this.showTemplateSelectorMenu();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -32,6 +32,10 @@ export default class FileTemplateSelector {
|
|||
}
|
||||
}
|
||||
|
||||
isHidden() {
|
||||
return this.$wrapper.hasClass('hidden');
|
||||
}
|
||||
|
||||
getToggleText() {
|
||||
return this.$dropdownToggleText.text();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import Sortable from 'vendor/Sortable';
|
|||
import Vue from 'vue';
|
||||
import AccessorUtilities from '../../lib/utils/accessor';
|
||||
import boardList from './board_list.vue';
|
||||
import boardBlankState from './board_blank_state';
|
||||
import BoardBlankState from './board_blank_state.vue';
|
||||
import './board_delete';
|
||||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
@ -18,7 +18,7 @@ gl.issueBoards.Board = Vue.extend({
|
|||
components: {
|
||||
boardList,
|
||||
'board-delete': gl.issueBoards.BoardDelete,
|
||||
boardBlankState,
|
||||
BoardBlankState,
|
||||
},
|
||||
props: {
|
||||
list: Object,
|
||||
|
|
|
@ -1,42 +1,11 @@
|
|||
<script>
|
||||
/* global ListLabel */
|
||||
|
||||
import _ from 'underscore';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
export default {
|
||||
template: `
|
||||
<div class="board-blank-state">
|
||||
<p>
|
||||
Add the following default lists to your Issue Board with one click:
|
||||
</p>
|
||||
<ul class="board-blank-state-list">
|
||||
<li v-for="label in predefinedLabels">
|
||||
<span
|
||||
class="label-color"
|
||||
:style="{ backgroundColor: label.color }">
|
||||
</span>
|
||||
{{ label.title }}
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Starting out with the default set of lists will get you right on the way to making the most of your board.
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-create btn-inverted btn-block"
|
||||
type="button"
|
||||
@click.stop="addDefaultLists">
|
||||
Add default lists
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
type="button"
|
||||
@click.stop="clearBlankState">
|
||||
Nevermind, I'll use my own
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
predefinedLabels: [
|
||||
|
@ -89,3 +58,41 @@ export default {
|
|||
clearBlankState: Store.removeBlankState.bind(Store),
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="board-blank-state">
|
||||
<p>
|
||||
Add the following default lists to your Issue Board with one click:
|
||||
</p>
|
||||
<ul class="board-blank-state-list">
|
||||
<li
|
||||
v-for="(label, index) in predefinedLabels"
|
||||
:key="index"
|
||||
>
|
||||
<span
|
||||
class="label-color"
|
||||
:style="{ backgroundColor: label.color }">
|
||||
</span>
|
||||
{{ label.title }}
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Starting out with the default set of lists will get you
|
||||
right on the way to making the most of your board.
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-create btn-inverted btn-block"
|
||||
type="button"
|
||||
@click.stop="addDefaultLists">
|
||||
Add default lists
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
type="button"
|
||||
@click.stop="clearBlankState">
|
||||
Nevermind, I'll use my own
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -1,9 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalEmptyState = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
|
|
|
@ -3,11 +3,11 @@ import Flash from '../../../flash';
|
|||
import { __ } from '../../../locale';
|
||||
import './lists_dropdown';
|
||||
import { pluralize } from '../../../lib/utils/text_utility';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalFooter = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import Vue from 'vue';
|
||||
import modalFilters from './filters';
|
||||
import './tabs';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalHeader = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
props: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
|
|
|
@ -7,8 +7,7 @@ import './header';
|
|||
import './list';
|
||||
import './footer';
|
||||
import './empty_state';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
|
||||
gl.issueBoards.IssuesModal = Vue.extend({
|
||||
props: {
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
import bp from '../../../breakpoints';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
|
||||
gl.issueBoards.ModalList = Vue.extend({
|
||||
props: {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
|
||||
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
|
||||
data() {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalTabs = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
|
|
|
@ -17,9 +17,9 @@ import './models/milestone';
|
|||
import './models/project';
|
||||
import './models/assignee';
|
||||
import './stores/boards_store';
|
||||
import './stores/modal_store';
|
||||
import ModalStore from './stores/modal_store';
|
||||
import BoardService from './services/board_service';
|
||||
import './mixins/modal_mixins';
|
||||
import modalMixin from './mixins/modal_mixins';
|
||||
import './mixins/sortable_default_options';
|
||||
import './filters/due_date_filters';
|
||||
import './components/board';
|
||||
|
@ -31,7 +31,6 @@ import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/fi
|
|||
export default () => {
|
||||
const $boardApp = document.getElementById('board-app');
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
|
||||
|
@ -176,7 +175,7 @@ export default () => {
|
|||
|
||||
gl.IssueBoardsModalAddBtn = new Vue({
|
||||
el: document.getElementById('js-add-issues-btn'),
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../stores/modal_store';
|
||||
|
||||
gl.issueBoards.ModalMixins = {
|
||||
export default {
|
||||
methods: {
|
||||
toggleModal(toggle) {
|
||||
ModalStore.store.showAddIssuesModal = toggle;
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
class ModalStore {
|
||||
constructor() {
|
||||
this.store = {
|
||||
|
@ -95,4 +92,4 @@ class ModalStore {
|
|||
}
|
||||
}
|
||||
|
||||
gl.issueBoards.ModalStore = new ModalStore();
|
||||
export default new ModalStore();
|
||||
|
|
|
@ -16,6 +16,7 @@ class DeleteModal {
|
|||
bindEvents() {
|
||||
this.$toggleBtns.on('click', this.setModalData.bind(this));
|
||||
this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
|
||||
this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this));
|
||||
}
|
||||
|
||||
setModalData(e) {
|
||||
|
@ -30,6 +31,16 @@ class DeleteModal {
|
|||
this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName);
|
||||
}
|
||||
|
||||
setDisableDeleteButton(e) {
|
||||
if (this.$deleteBtn.is('[disabled]')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
updateModal() {
|
||||
this.$branchName.text(this.branchName);
|
||||
this.$confirmInput.val('');
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
import Flash from '../flash';
|
||||
import { s__ } from '../locale';
|
||||
import setupToggleButtons from '../toggle_buttons';
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import setupToggleButtons from '~/toggle_buttons';
|
||||
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
|
||||
|
||||
import ClustersService from './services/clusters_service';
|
||||
|
||||
export default () => {
|
||||
const clusterList = document.querySelector('.js-clusters-list');
|
||||
|
||||
gcpSignupOffer();
|
||||
|
||||
// The empty state won't have a clusterList
|
||||
if (clusterList) {
|
||||
setupToggleButtons(
|
||||
document.querySelector('.js-clusters-list'),
|
||||
(value, toggle) =>
|
||||
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } })
|
||||
.catch((err) => {
|
||||
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
|
||||
throw err;
|
||||
}),
|
||||
setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
|
||||
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
|
||||
err => {
|
||||
createFlash(__('Something went wrong on our end.'));
|
||||
throw err;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,96 +1,102 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { s__, sprintf } from '../../locale';
|
||||
import applicationRow from './application_row.vue';
|
||||
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
|
||||
import {
|
||||
APPLICATION_INSTALLED,
|
||||
INGRESS,
|
||||
} from '../constants';
|
||||
import _ from 'underscore';
|
||||
import { s__, sprintf } from '../../locale';
|
||||
import applicationRow from './application_row.vue';
|
||||
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
|
||||
import { APPLICATION_INSTALLED, INGRESS } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
applicationRow,
|
||||
clipboardButton,
|
||||
export default {
|
||||
components: {
|
||||
applicationRow,
|
||||
clipboardButton,
|
||||
},
|
||||
props: {
|
||||
applications: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
props: {
|
||||
applications: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
helpPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
ingressHelpPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
ingressDnsHelpPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
managePrometheusPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
helpPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
computed: {
|
||||
generalApplicationDescription() {
|
||||
return sprintf(
|
||||
_.escape(s__(
|
||||
ingressHelpPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
ingressDnsHelpPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
managePrometheusPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
generalApplicationDescription() {
|
||||
return sprintf(
|
||||
_.escape(
|
||||
s__(
|
||||
`ClusterIntegration|Install applications on your Kubernetes cluster.
|
||||
Read more about %{helpLink}`,
|
||||
)), {
|
||||
helpLink: `<a href="${this.helpPath}">
|
||||
),
|
||||
),
|
||||
{
|
||||
helpLink: `<a href="${this.helpPath}">
|
||||
${_.escape(s__('ClusterIntegration|installing applications'))}
|
||||
</a>`,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
ingressId() {
|
||||
return INGRESS;
|
||||
},
|
||||
ingressInstalled() {
|
||||
return this.applications.ingress.status === APPLICATION_INSTALLED;
|
||||
},
|
||||
ingressExternalIp() {
|
||||
return this.applications.ingress.externalIp;
|
||||
},
|
||||
ingressDescription() {
|
||||
const extraCostParagraph = sprintf(
|
||||
_.escape(s__(
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
ingressId() {
|
||||
return INGRESS;
|
||||
},
|
||||
ingressInstalled() {
|
||||
return this.applications.ingress.status === APPLICATION_INSTALLED;
|
||||
},
|
||||
ingressExternalIp() {
|
||||
return this.applications.ingress.externalIp;
|
||||
},
|
||||
ingressDescription() {
|
||||
const extraCostParagraph = sprintf(
|
||||
_.escape(
|
||||
s__(
|
||||
`ClusterIntegration|%{boldNotice} This will add some extra resources
|
||||
like a load balancer, which may incur additional costs depending on
|
||||
the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
|
||||
you can %{pricingLink}.`,
|
||||
)), {
|
||||
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
|
||||
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
|
||||
the hosting provider your Kubernetes cluster is installed on. If you are using
|
||||
Google Kubernetes Engine, you can %{pricingLink}.`,
|
||||
),
|
||||
),
|
||||
{
|
||||
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
|
||||
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
|
||||
${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const externalIpParagraph = sprintf(
|
||||
_.escape(s__(
|
||||
const externalIpParagraph = sprintf(
|
||||
_.escape(
|
||||
s__(
|
||||
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
|
||||
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
|
||||
)), {
|
||||
ingressHelpLink: `<a href="${this.ingressHelpPath}">
|
||||
),
|
||||
),
|
||||
{
|
||||
ingressHelpLink: `<a href="${this.ingressHelpPath}">
|
||||
${_.escape(s__('ClusterIntegration|More information'))}
|
||||
</a>`,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
return `
|
||||
return `
|
||||
<p>
|
||||
${extraCostParagraph}
|
||||
</p>
|
||||
|
@ -98,22 +104,25 @@
|
|||
${externalIpParagraph}
|
||||
</p>
|
||||
`;
|
||||
},
|
||||
prometheusDescription() {
|
||||
return sprintf(
|
||||
_.escape(s__(
|
||||
},
|
||||
prometheusDescription() {
|
||||
return sprintf(
|
||||
_.escape(
|
||||
s__(
|
||||
`ClusterIntegration|Prometheus is an open-source monitoring system
|
||||
with %{gitlabIntegrationLink} to monitor deployed applications.`,
|
||||
)), {
|
||||
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
|
||||
),
|
||||
),
|
||||
{
|
||||
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -205,7 +214,7 @@
|
|||
>
|
||||
{{ s__(`ClusterIntegration|The IP address is in
|
||||
the process of being assigned. Please check your Kubernetes
|
||||
cluster or Quotas on GKE if it takes a long time.`) }}
|
||||
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
|
||||
|
||||
<a
|
||||
:href="ingressHelpPath"
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import $ from 'jquery';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
import Flash from '~/flash';
|
||||
|
||||
export default function gcpSignupOffer() {
|
||||
const alertEl = document.querySelector('.gcp-signup-offer');
|
||||
if (!alertEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closeButtonEl = alertEl.getElementsByClassName('close')[0];
|
||||
const { dismissEndpoint, featureId } = closeButtonEl.dataset;
|
||||
|
||||
closeButtonEl.addEventListener('click', () => {
|
||||
axios
|
||||
.post(dismissEndpoint, {
|
||||
feature_name: featureId,
|
||||
})
|
||||
.then(() => {
|
||||
$(alertEl).alert('close');
|
||||
})
|
||||
.catch(() => {
|
||||
Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -55,22 +55,20 @@
|
|||
},
|
||||
methods: {
|
||||
successCallback(resp) {
|
||||
return resp.json().then((response) => {
|
||||
// depending of the endpoint the response can either bring a `pipelines` key or not.
|
||||
const pipelines = response.pipelines || response;
|
||||
this.setCommonData(pipelines);
|
||||
// depending of the endpoint the response can either bring a `pipelines` key or not.
|
||||
const pipelines = resp.data.pipelines || resp.data;
|
||||
this.setCommonData(pipelines);
|
||||
|
||||
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
|
||||
detail: {
|
||||
pipelines: response,
|
||||
},
|
||||
});
|
||||
|
||||
// notifiy to update the count in tabs
|
||||
if (this.$el.parentElement) {
|
||||
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
|
||||
}
|
||||
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
|
||||
detail: {
|
||||
pipelines: resp.data,
|
||||
},
|
||||
});
|
||||
|
||||
// notifiy to update the count in tabs
|
||||
if (this.$el.parentElement) {
|
||||
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
|
||||
|
||||
import $ from 'jquery';
|
||||
import { localTimeAgo } from './lib/utils/datetime_utility';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
|
||||
export default class Compare {
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.source_loading = $(".js-source-loading");
|
||||
this.target_loading = $(".js-target-loading");
|
||||
$('.js-compare-dropdown').each((function(_this) {
|
||||
return function(i, dropdown) {
|
||||
var $dropdown;
|
||||
$dropdown = $(dropdown);
|
||||
return $dropdown.glDropdown({
|
||||
selectable: true,
|
||||
fieldName: $dropdown.data('fieldName'),
|
||||
filterable: true,
|
||||
id: function(obj, $el) {
|
||||
return $el.data('id');
|
||||
},
|
||||
toggleLabel: function(obj, $el) {
|
||||
return $el.text().trim();
|
||||
},
|
||||
clicked: function(e, el) {
|
||||
if ($dropdown.is('.js-target-branch')) {
|
||||
return _this.getTargetHtml();
|
||||
} else if ($dropdown.is('.js-source-branch')) {
|
||||
return _this.getSourceHtml();
|
||||
} else if ($dropdown.is('.js-target-project')) {
|
||||
return _this.getTargetProject();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
})(this));
|
||||
this.initialState();
|
||||
}
|
||||
|
||||
initialState() {
|
||||
this.getSourceHtml();
|
||||
this.getTargetHtml();
|
||||
}
|
||||
|
||||
getTargetProject() {
|
||||
$('.mr_target_commit').empty();
|
||||
|
||||
return axios.get(this.opts.targetProjectUrl, {
|
||||
params: {
|
||||
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
$('.js-target-branch-dropdown .dropdown-content').html(data);
|
||||
});
|
||||
}
|
||||
|
||||
getSourceHtml() {
|
||||
return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
|
||||
ref: $("input[name='merge_request[source_branch]']").val()
|
||||
});
|
||||
}
|
||||
|
||||
getTargetHtml() {
|
||||
return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
|
||||
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
|
||||
ref: $("input[name='merge_request[target_branch]']").val()
|
||||
});
|
||||
}
|
||||
|
||||
static sendAjax(url, loading, target, params) {
|
||||
const $target = $(target);
|
||||
|
||||
loading.show();
|
||||
$target.empty();
|
||||
|
||||
return axios.get(url, {
|
||||
params,
|
||||
}).then(({ data }) => {
|
||||
loading.hide();
|
||||
$target.html(data);
|
||||
const className = '.' + $target[0].className.replace(' ', '.');
|
||||
localTimeAgo($('.js-timeago', className));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -4,8 +4,9 @@ import $ from 'jquery';
|
|||
import { __ } from './locale';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
import flash from './flash';
|
||||
import { capitalizeFirstCharacter } from './lib/utils/text_utility';
|
||||
|
||||
export default function initCompareAutocomplete() {
|
||||
export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
|
||||
$('.js-compare-dropdown').each(function() {
|
||||
var $dropdown, selected;
|
||||
$dropdown = $(this);
|
||||
|
@ -15,14 +16,27 @@ export default function initCompareAutocomplete() {
|
|||
const $filterInput = $('input[type="search"]', $dropdownContainer);
|
||||
$dropdown.glDropdown({
|
||||
data: function(term, callback) {
|
||||
axios.get($dropdown.data('refsUrl'), {
|
||||
params: {
|
||||
ref: $dropdown.data('ref'),
|
||||
search: term,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
callback(data);
|
||||
}).catch(() => flash(__('Error fetching refs')));
|
||||
const params = {
|
||||
ref: $dropdown.data('ref'),
|
||||
search: term,
|
||||
};
|
||||
|
||||
if (limitTo) {
|
||||
params.find = limitTo;
|
||||
}
|
||||
|
||||
axios
|
||||
.get($dropdown.data('refsUrl'), {
|
||||
params,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (limitTo) {
|
||||
callback(data[capitalizeFirstCharacter(limitTo)] || []);
|
||||
} else {
|
||||
callback(data);
|
||||
}
|
||||
})
|
||||
.catch(() => flash(__('Error fetching refs')));
|
||||
},
|
||||
selectable: true,
|
||||
filterable: true,
|
||||
|
@ -32,9 +46,15 @@ export default function initCompareAutocomplete() {
|
|||
renderRow: function(ref) {
|
||||
var link;
|
||||
if (ref.header != null) {
|
||||
return $('<li />').addClass('dropdown-header').text(ref.header);
|
||||
return $('<li />')
|
||||
.addClass('dropdown-header')
|
||||
.text(ref.header);
|
||||
} else {
|
||||
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
|
||||
link = $('<a />')
|
||||
.attr('href', '#')
|
||||
.addClass(ref === selected ? 'is-active' : '')
|
||||
.text(ref)
|
||||
.attr('data-ref', ref);
|
||||
return $('<li />').append(link);
|
||||
}
|
||||
},
|
||||
|
@ -43,9 +63,10 @@ export default function initCompareAutocomplete() {
|
|||
},
|
||||
toggleLabel: function(obj, $el) {
|
||||
return $el.text().trim();
|
||||
}
|
||||
},
|
||||
clicked: () => clickHandler($dropdown),
|
||||
});
|
||||
$filterInput.on('keyup', (e) => {
|
||||
$filterInput.on('keyup', e => {
|
||||
const keyCode = e.keyCode || e.which;
|
||||
if (keyCode !== 13) return;
|
||||
const text = $filterInput.val();
|
||||
|
@ -54,7 +75,7 @@ export default function initCompareAutocomplete() {
|
|||
$dropdownContainer.removeClass('open');
|
||||
});
|
||||
|
||||
$dropdownContainer.on('click', '.dropdown-content a', (e) => {
|
||||
$dropdownContainer.on('click', '.dropdown-content a', e => {
|
||||
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
|
||||
if ($dropdown.hasClass('has-tooltip')) {
|
||||
$dropdown.tooltip('fixTitle');
|
||||
|
|
|
@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
|
|||
if (data.can_create_branch) {
|
||||
this.available();
|
||||
this.enable();
|
||||
this.updateBranchName(data.suggested_branch_name);
|
||||
|
||||
if (!this.droplabInitialized) {
|
||||
this.droplabInitialized = true;
|
||||
this.initDroplab();
|
||||
this.bindEvents();
|
||||
}
|
||||
} else if (data.has_related_branch) {
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.unavailable();
|
||||
this.disable();
|
||||
Flash('Failed to check if a new branch can be created.');
|
||||
Flash(__('Failed to check related branches.'));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown {
|
|||
this.unavailableButton.classList.remove('hide');
|
||||
}
|
||||
|
||||
updateBranchName(suggestedBranchName) {
|
||||
this.branchInput.value = suggestedBranchName;
|
||||
this.updateCreatePaths('branch', suggestedBranchName);
|
||||
}
|
||||
|
||||
updateInputState(target, ref, result) {
|
||||
// target - 'branch' or 'ref' - which the input field we are searching a ref for.
|
||||
// ref - string - what a user typed.
|
||||
// result - string - what has been found on backend.
|
||||
|
||||
const pathReplacement = `$1${ref}`;
|
||||
|
||||
// If a found branch equals exact the same text a user typed,
|
||||
// that means a new branch cannot be created as it already exists.
|
||||
if (ref === result) {
|
||||
|
@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
|
|||
this.refIsValid = true;
|
||||
this.refInput.dataset.value = ref;
|
||||
this.showAvailableMessage('ref');
|
||||
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
|
||||
pathReplacement);
|
||||
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
|
||||
pathReplacement);
|
||||
this.updateCreatePaths(target, ref);
|
||||
}
|
||||
} else if (target === 'branch') {
|
||||
this.branchIsValid = true;
|
||||
this.showAvailableMessage('branch');
|
||||
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
|
||||
pathReplacement);
|
||||
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
|
||||
pathReplacement);
|
||||
this.updateCreatePaths(target, ref);
|
||||
} else {
|
||||
this.refIsValid = false;
|
||||
this.refInput.dataset.value = ref;
|
||||
|
@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
|
|||
this.disableCreateAction();
|
||||
}
|
||||
}
|
||||
|
||||
// target - 'branch' or 'ref'
|
||||
// ref - string - the new value to use as branch or ref
|
||||
updateCreatePaths(target, ref) {
|
||||
const pathReplacement = `$1${ref}`;
|
||||
|
||||
this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
|
||||
pathReplacement);
|
||||
this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
|
||||
pathReplacement);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +1,50 @@
|
|||
<script>
|
||||
import eventHub from '../eventhub';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import eventHub from '../eventhub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
loadingIcon,
|
||||
export default {
|
||||
components: {
|
||||
loadingIcon,
|
||||
},
|
||||
props: {
|
||||
deployKey: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
deployKey: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
btnCssClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'btn-default',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
btnCssClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'btn-default',
|
||||
},
|
||||
computed: {
|
||||
text() {
|
||||
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
doAction() {
|
||||
this.isLoading = true;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
doAction() {
|
||||
this.isLoading = true;
|
||||
|
||||
eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="btn btn-sm prepend-left-10"
|
||||
class="btn"
|
||||
:class="[{ disabled: isLoading }, btnCssClass]"
|
||||
:disabled="isLoading"
|
||||
@click="doAction">
|
||||
{{ text }}
|
||||
<slot></slot>
|
||||
<loading-icon
|
||||
v-if="isLoading"
|
||||
:inline="true"
|
||||
|
|
|
@ -1,80 +1,115 @@
|
|||
<script>
|
||||
import Flash from '../../flash';
|
||||
import eventHub from '../eventhub';
|
||||
import DeployKeysService from '../service';
|
||||
import DeployKeysStore from '../store';
|
||||
import keysPanel from './keys_panel.vue';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import { s__ } from '~/locale';
|
||||
import Flash from '~/flash';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
|
||||
import eventHub from '../eventhub';
|
||||
import DeployKeysService from '../service';
|
||||
import DeployKeysStore from '../store';
|
||||
import KeysPanel from './keys_panel.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
keysPanel,
|
||||
loadingIcon,
|
||||
export default {
|
||||
components: {
|
||||
KeysPanel,
|
||||
LoadingIcon,
|
||||
NavigationTabs,
|
||||
},
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
store: new DeployKeysStore(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasKeys() {
|
||||
return Object.keys(this.keys).length;
|
||||
},
|
||||
keys() {
|
||||
return this.store.keys;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.service = new DeployKeysService(this.endpoint);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTab: 'enabled_keys',
|
||||
isLoading: false,
|
||||
store: new DeployKeysStore(),
|
||||
};
|
||||
},
|
||||
scopes: {
|
||||
enabled_keys: s__('DeployKeys|Enabled deploy keys'),
|
||||
available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
|
||||
public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
|
||||
},
|
||||
computed: {
|
||||
tabs() {
|
||||
return Object.keys(this.$options.scopes).map(scope => {
|
||||
const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null;
|
||||
|
||||
eventHub.$on('enable.key', this.enableKey);
|
||||
eventHub.$on('remove.key', this.disableKey);
|
||||
eventHub.$on('disable.key', this.disableKey);
|
||||
return {
|
||||
name: this.$options.scopes[scope],
|
||||
scope,
|
||||
isActive: scope === this.currentTab,
|
||||
count,
|
||||
};
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.fetchKeys();
|
||||
hasKeys() {
|
||||
return Object.keys(this.keys).length;
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('enable.key', this.enableKey);
|
||||
eventHub.$off('remove.key', this.disableKey);
|
||||
eventHub.$off('disable.key', this.disableKey);
|
||||
keys() {
|
||||
return this.store.keys;
|
||||
},
|
||||
methods: {
|
||||
fetchKeys() {
|
||||
this.isLoading = true;
|
||||
},
|
||||
created() {
|
||||
this.service = new DeployKeysService(this.endpoint);
|
||||
|
||||
this.service.getKeys()
|
||||
.then((data) => {
|
||||
this.isLoading = false;
|
||||
this.store.keys = data;
|
||||
})
|
||||
.catch(() => new Flash('Error getting deploy keys'));
|
||||
},
|
||||
enableKey(deployKey) {
|
||||
this.service.enableKey(deployKey.id)
|
||||
.then(() => this.fetchKeys())
|
||||
.catch(() => new Flash('Error enabling deploy key'));
|
||||
},
|
||||
disableKey(deployKey, callback) {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (confirm('You are going to remove this deploy key. Are you sure?')) {
|
||||
this.service.disableKey(deployKey.id)
|
||||
.then(() => this.fetchKeys())
|
||||
.then(callback)
|
||||
.catch(() => new Flash('Error removing deploy key'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
eventHub.$on('enable.key', this.enableKey);
|
||||
eventHub.$on('remove.key', this.disableKey);
|
||||
eventHub.$on('disable.key', this.disableKey);
|
||||
},
|
||||
mounted() {
|
||||
this.fetchKeys();
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('enable.key', this.enableKey);
|
||||
eventHub.$off('remove.key', this.disableKey);
|
||||
eventHub.$off('disable.key', this.disableKey);
|
||||
},
|
||||
methods: {
|
||||
onChangeTab(tab) {
|
||||
this.currentTab = tab;
|
||||
},
|
||||
};
|
||||
fetchKeys() {
|
||||
this.isLoading = true;
|
||||
|
||||
return this.service
|
||||
.getKeys()
|
||||
.then(data => {
|
||||
this.isLoading = false;
|
||||
this.store.keys = data;
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
this.store.keys = {};
|
||||
return new Flash(s__('DeployKeys|Error getting deploy keys'));
|
||||
});
|
||||
},
|
||||
enableKey(deployKey) {
|
||||
this.service
|
||||
.enableKey(deployKey.id)
|
||||
.then(this.fetchKeys)
|
||||
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
|
||||
},
|
||||
disableKey(deployKey, callback) {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
|
||||
this.service
|
||||
.disableKey(deployKey.id)
|
||||
.then(this.fetchKeys)
|
||||
.then(callback)
|
||||
.catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -82,29 +117,38 @@
|
|||
<loading-icon
|
||||
v-if="isLoading && !hasKeys"
|
||||
size="2"
|
||||
label="Loading deploy keys"
|
||||
:label="s__('DeployKeys|Loading deploy keys')"
|
||||
/>
|
||||
<div v-else-if="hasKeys">
|
||||
<template v-else-if="hasKeys">
|
||||
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
|
||||
<div class="fade-left">
|
||||
<i
|
||||
class="fa fa-angle-left"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</div>
|
||||
<div class="fade-right">
|
||||
<i
|
||||
class="fa fa-angle-right"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</div>
|
||||
|
||||
<navigation-tabs
|
||||
:tabs="tabs"
|
||||
@onChangeTab="onChangeTab"
|
||||
scope="deployKeys"
|
||||
/>
|
||||
</div>
|
||||
<keys-panel
|
||||
title="Enabled deploy keys for this project"
|
||||
class="qa-project-deploy-keys"
|
||||
:keys="keys.enabled_keys"
|
||||
:project-id="projectId"
|
||||
:keys="keys[currentTab]"
|
||||
:store="store"
|
||||
:endpoint="endpoint"
|
||||
/>
|
||||
<keys-panel
|
||||
title="Deploy keys from projects you have access to"
|
||||
:keys="keys.available_project_keys"
|
||||
:store="store"
|
||||
:endpoint="endpoint"
|
||||
/>
|
||||
<keys-panel
|
||||
v-if="keys.public_keys.length"
|
||||
title="Public deploy keys available to any project"
|
||||
:keys="keys.public_keys"
|
||||
:store="store"
|
||||
:endpoint="endpoint"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,111 +1,235 @@
|
|||
<script>
|
||||
import actionBtn from './action_btn.vue';
|
||||
import { getTimeago } from '../../lib/utils/datetime_utility';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import _ from 'underscore';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
actionBtn,
|
||||
import actionBtn from './action_btn.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
actionBtn,
|
||||
icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
props: {
|
||||
deployKey: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
deployKey: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
timeagoDate() {
|
||||
return getTimeago().format(this.deployKey.created_at);
|
||||
},
|
||||
editDeployKeyPath() {
|
||||
return `${this.endpoint}/${this.deployKey.id}/edit`;
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
methods: {
|
||||
isEnabled(id) {
|
||||
return this.store.findEnabledKey(id) !== undefined;
|
||||
},
|
||||
tooltipTitle(project) {
|
||||
return project.can_push ? 'Write access allowed' : 'Read access only';
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projectsExpanded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
editDeployKeyPath() {
|
||||
return `${this.endpoint}/${this.deployKey.id}/edit`;
|
||||
},
|
||||
};
|
||||
projects() {
|
||||
const projects = [...this.deployKey.deploy_keys_projects];
|
||||
|
||||
if (this.projectId !== null) {
|
||||
const indexOfCurrentProject = _.findIndex(
|
||||
projects,
|
||||
project =>
|
||||
project &&
|
||||
project.project &&
|
||||
project.project.id &&
|
||||
project.project.id.toString() === this.projectId,
|
||||
);
|
||||
|
||||
if (indexOfCurrentProject > -1) {
|
||||
const currentProject = projects.splice(indexOfCurrentProject, 1);
|
||||
currentProject[0].project.full_name = s__('DeployKeys|Current project');
|
||||
return currentProject.concat(projects);
|
||||
}
|
||||
}
|
||||
return projects;
|
||||
},
|
||||
firstProject() {
|
||||
return _.head(this.projects);
|
||||
},
|
||||
restProjects() {
|
||||
return _.tail(this.projects);
|
||||
},
|
||||
restProjectsTooltip() {
|
||||
return sprintf(s__('DeployKeys|Expand %{count} other projects'), {
|
||||
count: this.restProjects.length,
|
||||
});
|
||||
},
|
||||
restProjectsLabel() {
|
||||
return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length });
|
||||
},
|
||||
isEnabled() {
|
||||
return this.store.isEnabled(this.deployKey.id);
|
||||
},
|
||||
isRemovable() {
|
||||
return (
|
||||
this.store.isEnabled(this.deployKey.id) &&
|
||||
this.deployKey.destroyed_when_orphaned &&
|
||||
this.deployKey.almost_orphaned
|
||||
);
|
||||
},
|
||||
isExpandable() {
|
||||
return !this.projectsExpanded && this.restProjects.length > 1;
|
||||
},
|
||||
isExpanded() {
|
||||
return this.projectsExpanded || this.restProjects.length === 1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
projectTooltipTitle(project) {
|
||||
return project.can_push
|
||||
? s__('DeployKeys|Write access allowed')
|
||||
: s__('DeployKeys|Read access only');
|
||||
},
|
||||
toggleExpanded() {
|
||||
this.projectsExpanded = !this.projectsExpanded;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="pull-left append-right-10 hidden-xs">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-key key-icon"
|
||||
>
|
||||
</i>
|
||||
</div>
|
||||
<div class="deploy-key-content key-list-item-info">
|
||||
<strong class="title qa-key-title">
|
||||
{{ deployKey.title }}
|
||||
</strong>
|
||||
<div class="description qa-key-fingerprint">
|
||||
{{ deployKey.fingerprint }}
|
||||
<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">
|
||||
<strong class="title qa-key-title">
|
||||
{{ deployKey.title }}
|
||||
</strong>
|
||||
<div class="fingerprint qa-key-fingerprint">
|
||||
{{ deployKey.fingerprint }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="deploy-key-content prepend-left-default deploy-key-projects">
|
||||
<a
|
||||
v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects"
|
||||
:key="i"
|
||||
class="label deploy-project-label"
|
||||
:href="deployKeysProject.project.full_path"
|
||||
:title="tooltipTitle(deployKeysProject)"
|
||||
v-tooltip
|
||||
>
|
||||
{{ deployKeysProject.project.full_name }}
|
||||
<i
|
||||
v-if="!deployKeysProject.can_push"
|
||||
aria-hidden="true"
|
||||
class="fa fa-lock"
|
||||
>
|
||||
</i>
|
||||
</a>
|
||||
<div class="table-section section-30 section-wrap">
|
||||
<div
|
||||
role="rowheader"
|
||||
class="table-mobile-header">
|
||||
{{ s__('DeployKeys|Project usage') }}
|
||||
</div>
|
||||
<div class="table-mobile-content deploy-project-list">
|
||||
<template v-if="projects.length > 0">
|
||||
<a
|
||||
class="label deploy-project-label"
|
||||
:title="projectTooltipTitle(firstProject)"
|
||||
v-tooltip
|
||||
>
|
||||
<span>
|
||||
{{ firstProject.project.full_name }}
|
||||
</span>
|
||||
<icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
|
||||
</a>
|
||||
<a
|
||||
v-if="isExpandable"
|
||||
class="label deploy-project-label"
|
||||
@click="toggleExpanded"
|
||||
:title="restProjectsTooltip"
|
||||
v-tooltip
|
||||
>
|
||||
<span>{{ restProjectsLabel }}</span>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="isExpanded"
|
||||
v-for="deployKeysProject in restProjects"
|
||||
:key="deployKeysProject.project.full_path"
|
||||
class="label deploy-project-label"
|
||||
:href="deployKeysProject.project.full_path"
|
||||
:title="projectTooltipTitle(deployKeysProject)"
|
||||
v-tooltip
|
||||
>
|
||||
<span>
|
||||
{{ deployKeysProject.project.full_name }}
|
||||
</span>
|
||||
<icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/>
|
||||
</a>
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
class="text-secondary">{{ __('None') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="deploy-key-content">
|
||||
<span class="key-created-at">
|
||||
created {{ timeagoDate }}
|
||||
</span>
|
||||
<a
|
||||
v-if="deployKey.can_edit"
|
||||
class="btn btn-sm"
|
||||
:href="editDeployKeyPath"
|
||||
>
|
||||
Edit
|
||||
</a>
|
||||
<action-btn
|
||||
v-if="!isEnabled(deployKey.id)"
|
||||
:deploy-key="deployKey"
|
||||
type="enable"
|
||||
/>
|
||||
<action-btn
|
||||
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
|
||||
:deploy-key="deployKey"
|
||||
btn-css-class="btn-warning"
|
||||
type="remove"
|
||||
/>
|
||||
<action-btn
|
||||
v-else
|
||||
:deploy-key="deployKey"
|
||||
btn-css-class="btn-warning"
|
||||
type="disable"
|
||||
/>
|
||||
<div class="table-section section-15 text-right">
|
||||
<div
|
||||
role="rowheader"
|
||||
class="table-mobile-header">
|
||||
{{ __('Created') }}
|
||||
</div>
|
||||
<div class="table-mobile-content text-secondary key-created-at">
|
||||
<span
|
||||
:title="tooltipTitle(deployKey.created_at)"
|
||||
v-tooltip>
|
||||
<icon name="calendar"/>
|
||||
<span>{{ timeFormated(deployKey.created_at) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-section section-15 table-button-footer deploy-key-actions">
|
||||
<div class="btn-group table-action-buttons">
|
||||
<action-btn
|
||||
v-if="!isEnabled"
|
||||
:deploy-key="deployKey"
|
||||
type="enable"
|
||||
>
|
||||
{{ __('Enable') }}
|
||||
</action-btn>
|
||||
<a
|
||||
v-if="deployKey.can_edit"
|
||||
class="btn btn-default text-secondary"
|
||||
:href="editDeployKeyPath"
|
||||
:title="__('Edit')"
|
||||
data-container="body"
|
||||
v-tooltip
|
||||
>
|
||||
<icon name="pencil"/>
|
||||
</a>
|
||||
<action-btn
|
||||
v-if="isRemovable"
|
||||
:deploy-key="deployKey"
|
||||
btn-css-class="btn-danger"
|
||||
type="remove"
|
||||
:title="__('Remove')"
|
||||
data-container="body"
|
||||
v-tooltip
|
||||
>
|
||||
<icon name="remove"/>
|
||||
</action-btn>
|
||||
<action-btn
|
||||
v-else-if="isEnabled"
|
||||
:deploy-key="deployKey"
|
||||
btn-css-class="btn-warning"
|
||||
type="disable"
|
||||
:title="__('Disable')"
|
||||
data-container="body"
|
||||
v-tooltip
|
||||
>
|
||||
<icon name="cancel"/>
|
||||
</action-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,62 +1,68 @@
|
|||
<script>
|
||||
import key from './key.vue';
|
||||
import deployKey from './key.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
key,
|
||||
export default {
|
||||
components: {
|
||||
deployKey,
|
||||
},
|
||||
props: {
|
||||
keys: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
keys: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
showHelpBox: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="deploy-keys-panel">
|
||||
<h5>
|
||||
{{ title }}
|
||||
({{ keys.length }})
|
||||
</h5>
|
||||
<ul
|
||||
class="well-list"
|
||||
v-if="keys.length"
|
||||
>
|
||||
<li
|
||||
<div class="deploy-keys-panel table-holder">
|
||||
<template v-if="keys.length > 0">
|
||||
<div
|
||||
role="row"
|
||||
class="gl-responsive-table-row table-row-header">
|
||||
<div
|
||||
role="rowheader"
|
||||
class="table-section section-40">
|
||||
{{ s__('DeployKeys|Deploy key') }}
|
||||
</div>
|
||||
<div
|
||||
role="rowheader"
|
||||
class="table-section section-30">
|
||||
{{ s__('DeployKeys|Project usage') }}
|
||||
</div>
|
||||
<div
|
||||
role="rowheader"
|
||||
class="table-section section-15 text-right">
|
||||
{{ __('Created') }}
|
||||
</div>
|
||||
</div>
|
||||
<deploy-key
|
||||
v-for="deployKey in keys"
|
||||
:key="deployKey.id"
|
||||
>
|
||||
<key
|
||||
:deploy-key="deployKey"
|
||||
:store="store"
|
||||
:endpoint="endpoint"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
:deploy-key="deployKey"
|
||||
:store="store"
|
||||
:endpoint="endpoint"
|
||||
:project-id="projectId"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="settings-message text-center"
|
||||
v-else-if="showHelpBox"
|
||||
v-else
|
||||
>
|
||||
No deploy keys found. Create one with the form above.
|
||||
{{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
import Vue from 'vue';
|
||||
import deployKeysApp from './components/app.vue';
|
||||
|
||||
export default () => new Vue({
|
||||
el: document.getElementById('js-deploy-keys'),
|
||||
components: {
|
||||
deployKeysApp,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
endpoint: this.$options.el.dataset.endpoint,
|
||||
};
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('deploy-keys-app', {
|
||||
props: {
|
||||
endpoint: this.endpoint,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
export default () =>
|
||||
new Vue({
|
||||
el: document.getElementById('js-deploy-keys'),
|
||||
components: {
|
||||
deployKeysApp,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
endpoint: this.$options.el.dataset.endpoint,
|
||||
projectId: this.$options.el.dataset.projectId,
|
||||
};
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('deploy-keys-app', {
|
||||
props: {
|
||||
endpoint: this.endpoint,
|
||||
projectId: this.projectId,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -7,21 +7,24 @@ export default class DeployKeysService {
|
|||
constructor(endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
|
||||
this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
|
||||
enable: {
|
||||
method: 'PUT',
|
||||
url: `${this.endpoint}{/id}/enable`,
|
||||
this.resource = Vue.resource(
|
||||
`${this.endpoint}{/id}`,
|
||||
{},
|
||||
{
|
||||
enable: {
|
||||
method: 'PUT',
|
||||
url: `${this.endpoint}{/id}/enable`,
|
||||
},
|
||||
disable: {
|
||||
method: 'PUT',
|
||||
url: `${this.endpoint}{/id}/disable`,
|
||||
},
|
||||
},
|
||||
disable: {
|
||||
method: 'PUT',
|
||||
url: `${this.endpoint}{/id}/disable`,
|
||||
},
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
getKeys() {
|
||||
return this.resource.get()
|
||||
.then(response => response.json());
|
||||
return this.resource.get().then(response => response.json());
|
||||
}
|
||||
|
||||
enableKey(id) {
|
||||
|
|
|
@ -3,7 +3,7 @@ export default class DeployKeysStore {
|
|||
this.keys = {};
|
||||
}
|
||||
|
||||
findEnabledKey(id) {
|
||||
return this.keys.enabled_keys.find(key => key.id === id);
|
||||
isEnabled(id) {
|
||||
return this.keys.enabled_keys.some(key => key.id === id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
import $ from 'jquery';
|
||||
import Pikaday from 'pikaday';
|
||||
import { __ } from '~/locale';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
import { timeFor } from './lib/utils/datetime_utility';
|
||||
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
|
||||
|
||||
class DueDateSelect {
|
||||
|
@ -14,6 +16,7 @@ class DueDateSelect {
|
|||
this.$dropdownParent = $dropdownParent;
|
||||
this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
|
||||
this.$block = $block;
|
||||
this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
|
||||
this.$selectbox = $dropdown.closest('.selectbox');
|
||||
this.$value = $block.find('.value');
|
||||
this.$valueContent = $block.find('.value-content');
|
||||
|
@ -128,7 +131,8 @@ class DueDateSelect {
|
|||
|
||||
submitSelectedDate(isDropdown) {
|
||||
const selectedDateValue = this.datePayload[this.abilityName].due_date;
|
||||
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
|
||||
const hasDueDate = this.displayedDate !== 'No due date';
|
||||
const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
|
||||
|
||||
this.$loading.removeClass('hidden').fadeIn();
|
||||
|
||||
|
@ -145,10 +149,13 @@ class DueDateSelect {
|
|||
|
||||
return axios.put(this.issueUpdateURL, this.datePayload)
|
||||
.then(() => {
|
||||
const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
|
||||
if (isDropdown) {
|
||||
this.$dropdown.trigger('loaded.gl.dropdown');
|
||||
this.$dropdown.dropdown('toggle');
|
||||
}
|
||||
this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
|
||||
|
||||
return this.$loading.fadeOut();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export function getEmojiCategoryMap() {
|
|||
symbols: [],
|
||||
flags: [],
|
||||
};
|
||||
Object.keys(emojiMap).forEach((name) => {
|
||||
Object.keys(emojiMap).forEach(name => {
|
||||
const emoji = emojiMap[name];
|
||||
if (emojiCategoryMap[emoji.category]) {
|
||||
emojiCategoryMap[emoji.category].push(name);
|
||||
|
@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) {
|
|||
classList.push(fallbackSpriteClass);
|
||||
}
|
||||
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
|
||||
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
|
||||
const fallbackSpriteAttribute = opts.sprite
|
||||
? `data-fallback-sprite-class="${fallbackSpriteClass}"`
|
||||
: '';
|
||||
let contents = emojiInfo.moji;
|
||||
if (opts.forceFallback && !opts.sprite) {
|
||||
contents = emojiImageTag(name, fallbackImageSrc);
|
||||
|
|
|
@ -54,7 +54,8 @@ const unicodeSupportTestMap = {
|
|||
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
|
||||
// `4 *` because RGBA
|
||||
const indexOffset = 4 * pixelOffset;
|
||||
const hasColor = imageDataArray[indexOffset + 0] ||
|
||||
const hasColor =
|
||||
imageDataArray[indexOffset + 0] ||
|
||||
imageDataArray[indexOffset + 1] ||
|
||||
imageDataArray[indexOffset + 2];
|
||||
const isVisible = imageDataArray[indexOffset + 3];
|
||||
|
@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
|
|||
const fontSize = 16;
|
||||
function generateUnicodeSupportMap(testMap) {
|
||||
const testMapKeys = Object.keys(testMap);
|
||||
const numTestEntries = testMapKeys
|
||||
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
|
||||
const numTestEntries = testMapKeys.reduce((list, testKey) => list.concat(testMap[testKey]), [])
|
||||
.length;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = (2 * fontSize);
|
||||
canvas.height = (numTestEntries * fontSize);
|
||||
canvas.width = 2 * fontSize;
|
||||
canvas.height = numTestEntries * fontSize;
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
|
||||
// Write each emoji to the canvas vertically
|
||||
let writeIndex = 0;
|
||||
testMapKeys.forEach((testKey) => {
|
||||
testMapKeys.forEach(testKey => {
|
||||
const testEntry = testMap[testKey];
|
||||
[].concat(testEntry).forEach((emojiUnicode) => {
|
||||
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
|
||||
[].concat(testEntry).forEach(emojiUnicode => {
|
||||
ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2);
|
||||
writeIndex += 1;
|
||||
});
|
||||
});
|
||||
|
@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) {
|
|||
// Read from the canvas
|
||||
const resultMap = {};
|
||||
let readIndex = 0;
|
||||
testMapKeys.forEach((testKey) => {
|
||||
testMapKeys.forEach(testKey => {
|
||||
const testEntry = testMap[testKey];
|
||||
// This needs to be a `reduce` instead of `every` because we need to
|
||||
// keep the `readIndex` in sync from the writes by running all entries
|
||||
const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
|
||||
const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => {
|
||||
// Sample along the vertical-middle for a couple of characters
|
||||
const imageData = ctx.getImageData(
|
||||
0,
|
||||
(readIndex * fontSize) + (fontSize / 2),
|
||||
2 * fontSize,
|
||||
1,
|
||||
).data;
|
||||
const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1)
|
||||
.data;
|
||||
|
||||
let isValidEmoji = false;
|
||||
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
|
||||
const isLookingAtFirstChar = currentPixel < fontSize;
|
||||
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
|
||||
const isLookingAtSecondChar = currentPixel >= fontSize + fontSize / 2;
|
||||
// Check for the emoji somewhere along the row
|
||||
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
|
||||
isValidEmoji = true;
|
||||
|
||||
// Check to see that nothing is rendered next to the first character
|
||||
// to ensure that the ZWJ sequence rendered as one piece
|
||||
// Check to see that nothing is rendered next to the first character
|
||||
// to ensure that the ZWJ sequence rendered as one piece
|
||||
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
|
||||
isValidEmoji = false;
|
||||
break;
|
||||
|
@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() {
|
|||
if (isLocalStorageAvailable) {
|
||||
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
|
||||
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
|
||||
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
|
||||
window.localStorage.setItem(
|
||||
'gl-emoji-unicode-support-map',
|
||||
JSON.stringify(unicodeSupportMap),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
<div class="environments-container">
|
||||
|
||||
<loading-icon
|
||||
class="prepend-top-default"
|
||||
label="Loading environments"
|
||||
v-if="isLoading"
|
||||
size="3"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import playIconSvg from 'icons/_icon_play.svg';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
@ -8,9 +8,9 @@
|
|||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
components: {
|
||||
loadingIcon,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
actions: {
|
||||
|
@ -19,20 +19,16 @@
|
|||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
playIconSvg,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
title() {
|
||||
return 'Deploy to...';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickAction(endpoint) {
|
||||
this.isLoading = true;
|
||||
|
@ -65,7 +61,10 @@
|
|||
:disabled="isLoading"
|
||||
>
|
||||
<span>
|
||||
<span v-html="playIconSvg"></span>
|
||||
<icon
|
||||
name="play"
|
||||
:size="12"
|
||||
/>
|
||||
<i
|
||||
class="fa fa-caret-down"
|
||||
aria-hidden="true"
|
||||
|
@ -86,7 +85,10 @@
|
|||
:class="{ disabled: isActionDisabled(action) }"
|
||||
:disabled="isActionDisabled(action)"
|
||||
>
|
||||
<span v-html="playIconSvg"></span>
|
||||
<icon
|
||||
name="play"
|
||||
:size="12"
|
||||
/>
|
||||
<span>
|
||||
{{ action.name }}
|
||||
</span>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import { s__ } from '../../locale';
|
||||
|
||||
|
@ -6,6 +7,9 @@
|
|||
* Renders the external url link in environments table.
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
@ -15,7 +19,6 @@
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
title() {
|
||||
return s__('Environments|Open');
|
||||
|
@ -34,10 +37,9 @@
|
|||
:aria-label="title"
|
||||
:href="externalUrl"
|
||||
>
|
||||
<i
|
||||
class="fa fa-external-link"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
<icon
|
||||
name="external-link"
|
||||
:size="12"
|
||||
/>
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
@ -2,20 +2,22 @@
|
|||
/**
|
||||
* Renders the Monitoring (Metrics) link in environments table.
|
||||
*/
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
props: {
|
||||
monitoringUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
title() {
|
||||
return 'Monitoring';
|
||||
|
@ -33,10 +35,9 @@
|
|||
:title="title"
|
||||
:aria-label="title"
|
||||
>
|
||||
<i
|
||||
class="fa fa-area-chart"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
<icon
|
||||
name="chart"
|
||||
:size="12"
|
||||
/>
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
components: {
|
||||
loadingIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
retryUrl: {
|
||||
type: String,
|
||||
|
@ -24,13 +23,11 @@
|
|||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.isLoading = true;
|
||||
|
|
|
@ -3,14 +3,16 @@
|
|||
* Renders a terminal button to open a web terminal.
|
||||
* Used in environments table.
|
||||
*/
|
||||
import terminalIconSvg from 'icons/_icon_terminal.svg';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
props: {
|
||||
terminalPath: {
|
||||
type: String,
|
||||
|
@ -18,13 +20,6 @@
|
|||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
terminalIconSvg,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
title() {
|
||||
return 'Terminal';
|
||||
|
@ -40,7 +35,10 @@
|
|||
:title="title"
|
||||
:aria-label="title"
|
||||
:href="terminalPath"
|
||||
v-html="terminalIconSvg"
|
||||
>
|
||||
<icon
|
||||
name="terminal"
|
||||
:size="12"
|
||||
/>
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import {
|
||||
getSelector,
|
||||
togglePopover,
|
||||
inserted,
|
||||
mouseenter,
|
||||
mouseleave,
|
||||
} from './feature_highlight_helper';
|
||||
import {
|
||||
togglePopover,
|
||||
mouseenter,
|
||||
debouncedMouseleave,
|
||||
} from '../shared/popover';
|
||||
|
||||
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
|
||||
const $selector = $(getSelector(id));
|
||||
const $parent = $selector.parent();
|
||||
const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
|
||||
const hideOnScroll = togglePopover.bind($selector, false);
|
||||
const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
|
||||
|
||||
$selector
|
||||
// Setup popover
|
||||
|
@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
|
|||
`,
|
||||
})
|
||||
.on('mouseenter', mouseenter)
|
||||
.on('mouseleave', debouncedMouseleave)
|
||||
.on('mouseleave', debouncedMouseleave(debounceTimeout))
|
||||
.on('inserted.bs.popover', inserted)
|
||||
.on('show.bs.popover', () => {
|
||||
window.addEventListener('scroll', hideOnScroll);
|
||||
})
|
||||
.on('hide.bs.popover', () => {
|
||||
window.removeEventListener('scroll', hideOnScroll);
|
||||
window.addEventListener('scroll', hideOnScroll, { once: true });
|
||||
})
|
||||
// Display feature highlight
|
||||
.removeAttr('disabled');
|
||||
|
|
|
@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils';
|
|||
import { __ } from '../locale';
|
||||
import Flash from '../flash';
|
||||
import LazyLoader from '../lazy_loader';
|
||||
import { togglePopover } from '../shared/popover';
|
||||
|
||||
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
|
||||
|
||||
export function togglePopover(show) {
|
||||
const isAlreadyShown = this.hasClass('js-popover-show');
|
||||
if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
|
||||
return false;
|
||||
}
|
||||
this.popover(show ? 'show' : 'hide');
|
||||
this.toggleClass('disable-animation js-popover-show', show);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function dismiss(highlightId) {
|
||||
axios.post(this.attr('data-dismiss-endpoint'), {
|
||||
feature_name: highlightId,
|
||||
|
@ -27,23 +17,6 @@ export function dismiss(highlightId) {
|
|||
this.hide();
|
||||
}
|
||||
|
||||
export function mouseleave() {
|
||||
if (!$('.popover:hover').length > 0) {
|
||||
const $featureHighlight = $(this);
|
||||
togglePopover.call($featureHighlight, false);
|
||||
}
|
||||
}
|
||||
|
||||
export function mouseenter() {
|
||||
const $featureHighlight = $(this);
|
||||
|
||||
const showedPopover = togglePopover.call($featureHighlight, true);
|
||||
if (showedPopover) {
|
||||
$('.popover')
|
||||
.on('mouseleave', mouseleave.bind($featureHighlight));
|
||||
}
|
||||
}
|
||||
|
||||
export function inserted() {
|
||||
const popoverId = this.getAttribute('aria-describedby');
|
||||
const highlightId = this.dataset.highlight;
|
||||
|
|
|
@ -408,7 +408,10 @@ class GfmAutoComplete {
|
|||
|
||||
fetchData($input, at) {
|
||||
if (this.isLoadingData[at]) return;
|
||||
|
||||
this.isLoadingData[at] = true;
|
||||
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
|
||||
|
||||
if (this.cachedData[at]) {
|
||||
this.loadData($input, at, this.cachedData[at]);
|
||||
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
|
||||
|
@ -418,12 +421,14 @@ class GfmAutoComplete {
|
|||
GfmAutoComplete.glEmojiTag = glEmojiTag;
|
||||
})
|
||||
.catch(() => { this.isLoadingData[at] = false; });
|
||||
} else {
|
||||
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
|
||||
} else if (dataSource) {
|
||||
AjaxCache.retrieve(dataSource, true)
|
||||
.then((data) => {
|
||||
this.loadData($input, at, data);
|
||||
})
|
||||
.catch(() => { this.isLoadingData[at] = false; });
|
||||
} else {
|
||||
this.isLoadingData[at] = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,12 +7,12 @@ import { __ } from '~/locale';
|
|||
export default class GpgBadges {
|
||||
static fetch() {
|
||||
const badges = $('.js-loading-gpg-badge');
|
||||
const form = $('.commits-search-form');
|
||||
const tag = $('.js-signature-container');
|
||||
|
||||
badges.html('<i class="fa fa-spinner fa-spin"></i>');
|
||||
|
||||
const params = parseQueryStringIntoObject(form.serialize());
|
||||
return axios.get(form.data('signaturesPath'), { params })
|
||||
const params = parseQueryStringIntoObject(tag.serialize());
|
||||
return axios.get(tag.data('signaturesPath'), { params })
|
||||
.then(({ data }) => {
|
||||
data.signatures.forEach((signature) => {
|
||||
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
|
||||
|
|
106
app/assets/javascripts/ide/components/activity_bar.vue
Normal file
106
app/assets/javascripts/ide/components/activity_bar.vue
Normal file
|
@ -0,0 +1,106 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import { activityBarViews } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentProject', 'hasChanges']),
|
||||
...mapState(['currentActivityView']),
|
||||
goBackUrl() {
|
||||
return document.referrer || this.currentProject.web_url;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateActivityBarView']),
|
||||
},
|
||||
activityBarViews,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="ide-activity-bar">
|
||||
<ul class="list-unstyled">
|
||||
<li v-once>
|
||||
<a
|
||||
v-tooltip
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
:href="goBackUrl"
|
||||
class="ide-sidebar-link"
|
||||
:title="s__('IDE|Go back')"
|
||||
:aria-label="s__('IDE|Go back')"
|
||||
>
|
||||
<icon
|
||||
:size="16"
|
||||
name="go-back"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-tooltip
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
type="button"
|
||||
class="ide-sidebar-link js-ide-edit-mode"
|
||||
:class="{
|
||||
active: currentActivityView === $options.activityBarViews.edit
|
||||
}"
|
||||
@click.prevent="updateActivityBarView($options.activityBarViews.edit)"
|
||||
:title="s__('IDE|Edit')"
|
||||
:aria-label="s__('IDE|Edit')"
|
||||
>
|
||||
<icon
|
||||
name="code"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-tooltip
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
type="button"
|
||||
class="ide-sidebar-link js-ide-review-mode"
|
||||
:class="{
|
||||
active: currentActivityView === $options.activityBarViews.review
|
||||
}"
|
||||
@click.prevent="updateActivityBarView($options.activityBarViews.review)"
|
||||
:title="s__('IDE|Review')"
|
||||
:aria-label="s__('IDE|Review')"
|
||||
>
|
||||
<icon
|
||||
name="file-modified"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
<li v-show="hasChanges">
|
||||
<button
|
||||
v-tooltip
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
type="button"
|
||||
class="ide-sidebar-link js-ide-commit-mode"
|
||||
:class="{
|
||||
active: currentActivityView === $options.activityBarViews.commit
|
||||
}"
|
||||
@click.prevent="updateActivityBarView($options.activityBarViews.commit)"
|
||||
:title="s__('IDE|Commit')"
|
||||
:aria-label="s__('IDE|Commit')"
|
||||
>
|
||||
<icon
|
||||
name="commit"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
|
@ -1,31 +1,88 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { pluralize } from '~/lib/utils/text_utility';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
Icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showTooltip: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showStagedIcon: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
forceModifiedIcon: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
changedIcon() {
|
||||
return this.file.tempFile ? 'file-addition' : 'file-modified';
|
||||
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
|
||||
return this.file.tempFile && !this.forceModifiedIcon
|
||||
? `file-addition${suffix}`
|
||||
: `file-modified${suffix}`;
|
||||
},
|
||||
stagedIcon() {
|
||||
return `${this.changedIcon}-solid`;
|
||||
},
|
||||
changedIconClass() {
|
||||
return `multi-${this.changedIcon}`;
|
||||
return `multi-${this.changedIcon} pull-left`;
|
||||
},
|
||||
tooltipTitle() {
|
||||
if (!this.showTooltip) return undefined;
|
||||
|
||||
const type = this.file.tempFile ? 'addition' : 'modification';
|
||||
|
||||
if (this.file.changed && !this.file.staged) {
|
||||
return sprintf(__('Unstaged %{type}'), {
|
||||
type,
|
||||
});
|
||||
} else if (!this.file.changed && this.file.staged) {
|
||||
return sprintf(__('Staged %{type}'), {
|
||||
type,
|
||||
});
|
||||
} else if (this.file.changed && this.file.staged) {
|
||||
return sprintf(__('Unstaged and staged %{type}'), {
|
||||
type: pluralize(type),
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<icon
|
||||
:name="changedIcon"
|
||||
:size="12"
|
||||
:css-classes="`ide-file-changed-icon ${changedIconClass}`"
|
||||
/>
|
||||
<span
|
||||
v-tooltip
|
||||
:title="tooltipTitle"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
class="ide-file-changed-icon"
|
||||
>
|
||||
<icon
|
||||
v-if="file.changed || file.tempFile || file.staged"
|
||||
:name="changedIcon"
|
||||
:size="12"
|
||||
:css-classes="changedIconClass"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -1,41 +1,39 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import * as consts from '../../stores/modules/commit/constants';
|
||||
import RadioGroup from './radio_group.vue';
|
||||
import _ from 'underscore';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import * as consts from '../../stores/modules/commit/constants';
|
||||
import RadioGroup from './radio_group.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RadioGroup,
|
||||
export default {
|
||||
components: {
|
||||
RadioGroup,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
|
||||
commitToCurrentBranchText() {
|
||||
return sprintf(
|
||||
__('Commit to %{branchName} branch'),
|
||||
{ branchName: `<strong class="monospace">${_.escape(this.currentBranchId)}</strong>` },
|
||||
false,
|
||||
);
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentBranchId',
|
||||
]),
|
||||
newMergeRequestHelpText() {
|
||||
return sprintf(
|
||||
__('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
|
||||
{ branchName: this.currentBranchId },
|
||||
);
|
||||
},
|
||||
commitToCurrentBranchText() {
|
||||
return sprintf(
|
||||
__('Commit to %{branchName} branch'),
|
||||
{ branchName: `<strong>${this.currentBranchId}</strong>` },
|
||||
false,
|
||||
);
|
||||
},
|
||||
commitToNewBranchText() {
|
||||
return sprintf(
|
||||
__('Creates a new branch from %{branchName}'),
|
||||
{ branchName: this.currentBranchId },
|
||||
);
|
||||
},
|
||||
disableMergeRequestRadio() {
|
||||
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
|
||||
},
|
||||
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
|
||||
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
|
||||
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.disableMergeRequestRadio) {
|
||||
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('commit', ['updateCommitAction']),
|
||||
},
|
||||
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
|
||||
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
|
||||
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -53,13 +51,12 @@
|
|||
:value="$options.commitToNewBranch"
|
||||
:label="__('Create a new branch')"
|
||||
:show-input="true"
|
||||
:help-text="commitToNewBranchText"
|
||||
/>
|
||||
<radio-group
|
||||
:value="$options.commitToNewBranchMR"
|
||||
:label="__('Create a new branch and merge request')"
|
||||
:show-input="true"
|
||||
:help-text="newMergeRequestHelpText"
|
||||
:disabled="disableMergeRequestRadio"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="!lastCommitMsg"
|
||||
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
|
||||
>
|
||||
<div
|
||||
class="ide-commit-empty-state-container"
|
||||
>
|
||||
<div class="svg-content svg-80">
|
||||
<img :src="noChangesStateSvgPath" />
|
||||
</div>
|
||||
<div class="append-right-default prepend-left-default">
|
||||
<div
|
||||
class="text-content text-center"
|
||||
>
|
||||
<h4>
|
||||
{{ __('No changes') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ __('Edit files in the editor and commit changes here') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
171
app/assets/javascripts/ide/components/commit_sidebar/form.vue
Normal file
171
app/assets/javascripts/ide/components/commit_sidebar/form.vue
Normal file
|
@ -0,0 +1,171 @@
|
|||
<script>
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import LoadingButton from '~/vue_shared/components/loading_button.vue';
|
||||
import CommitMessageField from './message_field.vue';
|
||||
import Actions from './actions.vue';
|
||||
import SuccessMessage from './success_message.vue';
|
||||
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Actions,
|
||||
LoadingButton,
|
||||
CommitMessageField,
|
||||
SuccessMessage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCompact: true,
|
||||
componentHeight: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
|
||||
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
|
||||
...mapGetters(['hasChanges']),
|
||||
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
|
||||
overviewText() {
|
||||
return sprintf(
|
||||
__(
|
||||
'<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
|
||||
),
|
||||
{
|
||||
stagedFilesLength: this.stagedFiles.length,
|
||||
changedFilesLength: this.changedFiles.length,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentActivityView() {
|
||||
if (this.lastCommitMsg) {
|
||||
this.isCompact = false;
|
||||
} else {
|
||||
this.isCompact = !(
|
||||
this.currentActivityView === activityBarViews.commit &&
|
||||
window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
|
||||
);
|
||||
}
|
||||
},
|
||||
lastCommitMsg() {
|
||||
this.isCompact =
|
||||
this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateActivityBarView']),
|
||||
...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
|
||||
toggleIsSmall() {
|
||||
this.updateActivityBarView(activityBarViews.commit)
|
||||
.then(() => {
|
||||
this.isCompact = !this.isCompact;
|
||||
})
|
||||
.catch(e => {
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
beforeEnterTransition() {
|
||||
const elHeight = this.isCompact
|
||||
? this.$refs.formEl && this.$refs.formEl.offsetHeight
|
||||
: this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
|
||||
|
||||
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
|
||||
},
|
||||
enterTransition() {
|
||||
this.$nextTick(() => {
|
||||
const elHeight = this.isCompact
|
||||
? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
|
||||
: this.$refs.formEl && this.$refs.formEl.offsetHeight;
|
||||
|
||||
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
|
||||
});
|
||||
},
|
||||
afterEndTransition() {
|
||||
this.componentHeight = null;
|
||||
},
|
||||
},
|
||||
activityBarViews,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-form"
|
||||
:class="{
|
||||
'is-compact': isCompact,
|
||||
'is-full': !isCompact
|
||||
}"
|
||||
:style="{
|
||||
height: componentHeight ? `${componentHeight}px` : null,
|
||||
}"
|
||||
>
|
||||
<transition
|
||||
name="commit-form-slide-up"
|
||||
@before-enter="beforeEnterTransition"
|
||||
@enter="enterTransition"
|
||||
@after-enter="afterEndTransition"
|
||||
>
|
||||
<div
|
||||
v-if="isCompact"
|
||||
class="commit-form-compact"
|
||||
ref="compactEl"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!hasChanges"
|
||||
class="btn btn-primary btn-sm btn-block"
|
||||
@click="toggleIsSmall"
|
||||
>
|
||||
{{ __('Commit') }}
|
||||
</button>
|
||||
<p
|
||||
class="text-center"
|
||||
v-html="overviewText"
|
||||
></p>
|
||||
</div>
|
||||
<form
|
||||
v-if="!isCompact"
|
||||
class="form-horizontal"
|
||||
@submit.prevent.stop="commitChanges"
|
||||
ref="formEl"
|
||||
>
|
||||
<transition name="fade">
|
||||
<success-message
|
||||
v-show="lastCommitMsg"
|
||||
/>
|
||||
</transition>
|
||||
<commit-message-field
|
||||
:text="commitMessage"
|
||||
@input="updateCommitMessage"
|
||||
/>
|
||||
<div class="clearfix prepend-top-15">
|
||||
<actions />
|
||||
<loading-button
|
||||
:loading="submitCommitLoading"
|
||||
:disabled="commitButtonDisabled"
|
||||
container-class="btn btn-success btn-sm pull-left"
|
||||
:label="__('Commit')"
|
||||
@click="commitChanges"
|
||||
/>
|
||||
<button
|
||||
v-if="!discardDraftButtonDisabled"
|
||||
type="button"
|
||||
class="btn btn-default btn-sm pull-right"
|
||||
@click="discardDraft"
|
||||
>
|
||||
{{ __('Discard draft') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="btn btn-default btn-sm pull-right"
|
||||
@click="toggleIsSmall"
|
||||
>
|
||||
{{ __('Collapse') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
|
@ -1,66 +1,128 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import listItem from './list_item.vue';
|
||||
import listCollapsed from './list_collapsed.vue';
|
||||
import { mapActions } from 'vuex';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import ListItem from './list_item.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
listItem,
|
||||
listCollapsed,
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
ListItem,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
fileList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentProjectId',
|
||||
'currentBranchId',
|
||||
'rightPanelCollapsed',
|
||||
]),
|
||||
isCommitInfoShown() {
|
||||
return this.rightPanelCollapsed || this.fileList.length;
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.$emit('toggleCollapsed');
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
actionBtnText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
itemActionComponent: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
stagedList: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showActionButton: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
titleText() {
|
||||
return sprintf(__('%{title} changes'), {
|
||||
title: this.title,
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['stageAllChanges', 'unstageAllChanges']),
|
||||
actionBtnClicked() {
|
||||
this[this.action]();
|
||||
},
|
||||
setShowActionButton(show) {
|
||||
this.showActionButton = show;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'multi-file-commit-list': isCommitInfoShown
|
||||
}"
|
||||
class="ide-commit-list-container"
|
||||
>
|
||||
<list-collapsed
|
||||
v-if="rightPanelCollapsed"
|
||||
/>
|
||||
<template v-else>
|
||||
<ul
|
||||
v-if="fileList.length"
|
||||
class="list-unstyled append-bottom-0"
|
||||
<header
|
||||
class="multi-file-commit-panel-header"
|
||||
@mouseenter="setShowActionButton(true)"
|
||||
@mouseleave="setShowActionButton(false)"
|
||||
>
|
||||
<div
|
||||
class="multi-file-commit-panel-header-title"
|
||||
>
|
||||
<li
|
||||
v-for="file in fileList"
|
||||
:key="file.key"
|
||||
<icon
|
||||
v-once
|
||||
:name="iconName"
|
||||
:size="18"
|
||||
/>
|
||||
{{ titleText }}
|
||||
<span
|
||||
v-show="!showActionButton"
|
||||
class="ide-commit-file-count"
|
||||
>
|
||||
<list-item
|
||||
:file="file"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
{{ fileList.length }}
|
||||
</span>
|
||||
<button
|
||||
v-show="showActionButton"
|
||||
type="button"
|
||||
class="btn btn-blank btn-link ide-staged-action-btn"
|
||||
@click="actionBtnClicked"
|
||||
>
|
||||
{{ actionBtnText }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<ul
|
||||
v-if="fileList.length"
|
||||
class="multi-file-commit-list list-unstyled append-bottom-0"
|
||||
>
|
||||
<li
|
||||
v-for="file in fileList"
|
||||
:key="file.key"
|
||||
>
|
||||
<list-item
|
||||
:file="file"
|
||||
:action-component="itemActionComponent"
|
||||
:key-prefix="title"
|
||||
:staged-list="stagedList"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<p
|
||||
v-else
|
||||
class="multi-file-commit-list help-block"
|
||||
>
|
||||
{{ __('No changes') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,35 +1,110 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import { sprintf, n__, __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
files: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'addedFiles',
|
||||
'modifiedFiles',
|
||||
]),
|
||||
iconName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
addedFilesLength() {
|
||||
return this.files.filter(f => f.tempFile).length;
|
||||
},
|
||||
modifiedFilesLength() {
|
||||
return this.files.filter(f => !f.tempFile).length;
|
||||
},
|
||||
addedFilesIconClass() {
|
||||
return this.addedFilesLength ? 'multi-file-addition' : '';
|
||||
},
|
||||
modifiedFilesClass() {
|
||||
return this.modifiedFilesLength ? 'multi-file-modified' : '';
|
||||
},
|
||||
additionsTooltip() {
|
||||
return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
|
||||
type: this.title.toLowerCase(),
|
||||
});
|
||||
},
|
||||
modifiedTooltip() {
|
||||
return sprintf(
|
||||
n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
|
||||
{ type: this.title.toLowerCase() },
|
||||
);
|
||||
},
|
||||
titleTooltip() {
|
||||
return sprintf(__('%{title} changes'), { title: this.title });
|
||||
},
|
||||
additionIconName() {
|
||||
return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
|
||||
},
|
||||
modifiedIconName() {
|
||||
return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-list-collapsed text-center"
|
||||
>
|
||||
<icon
|
||||
name="file-addition"
|
||||
:size="18"
|
||||
css-classes="multi-file-addition append-bottom-10"
|
||||
/>
|
||||
{{ addedFiles.length }}
|
||||
<icon
|
||||
name="file-modified"
|
||||
:size="18"
|
||||
css-classes="multi-file-modified prepend-top-10 append-bottom-10"
|
||||
/>
|
||||
{{ modifiedFiles.length }}
|
||||
<div
|
||||
v-tooltip
|
||||
:title="titleTooltip"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
class="append-bottom-15"
|
||||
>
|
||||
<icon
|
||||
v-once
|
||||
:name="iconName"
|
||||
:size="18"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-tooltip
|
||||
:title="additionsTooltip"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
class="append-bottom-10"
|
||||
>
|
||||
<icon
|
||||
:name="additionIconName"
|
||||
:size="18"
|
||||
:css-classes="addedFilesIconClass"
|
||||
/>
|
||||
</div>
|
||||
{{ addedFilesLength }}
|
||||
<div
|
||||
v-tooltip
|
||||
:title="modifiedTooltip"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
class="prepend-top-10 append-bottom-10"
|
||||
>
|
||||
<icon
|
||||
:name="modifiedIconName"
|
||||
:size="18"
|
||||
:css-classes="modifiedFilesClass"
|
||||
/>
|
||||
</div>
|
||||
{{ modifiedFilesLength }}
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,34 +1,70 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import StageButton from './stage_button.vue';
|
||||
import UnstageButton from './unstage_button.vue';
|
||||
import { viewerTypes } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
StageButton,
|
||||
UnstageButton,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
actionComponent: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
keyPrefix: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
stagedList: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return this.file.tempFile ? 'file-addition' : 'file-modified';
|
||||
const prefix = this.stagedList ? '-solid' : '';
|
||||
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
|
||||
},
|
||||
iconClass() {
|
||||
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
|
||||
openFileInEditor(file) {
|
||||
return this.openPendingTab(file).then(changeViewer => {
|
||||
...mapActions([
|
||||
'discardFileChanges',
|
||||
'updateViewer',
|
||||
'openPendingTab',
|
||||
'unstageChange',
|
||||
'stageChange',
|
||||
]),
|
||||
openFileInEditor() {
|
||||
return this.openPendingTab({
|
||||
file: this.file,
|
||||
keyPrefix: this.keyPrefix.toLowerCase(),
|
||||
}).then(changeViewer => {
|
||||
if (changeViewer) {
|
||||
this.updateViewer('diff');
|
||||
this.updateViewer(viewerTypes.diff);
|
||||
}
|
||||
});
|
||||
},
|
||||
fileAction() {
|
||||
if (this.file.staged) {
|
||||
this.unstageChange(this.file.path);
|
||||
} else {
|
||||
this.stageChange(this.file.path);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -38,7 +74,9 @@ export default {
|
|||
<button
|
||||
type="button"
|
||||
class="multi-file-commit-list-path"
|
||||
@click="openFileInEditor(file)">
|
||||
@dblclick="fileAction"
|
||||
@click="openFileInEditor"
|
||||
>
|
||||
<span class="multi-file-commit-list-file-path">
|
||||
<icon
|
||||
:name="iconName"
|
||||
|
@ -47,12 +85,9 @@ export default {
|
|||
/>{{ file.path }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-blank multi-file-discard-btn"
|
||||
@click="discardFileChanges(file.path)"
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
<component
|
||||
:is="actionComponent"
|
||||
:path="file.path"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
<script>
|
||||
import { __, sprintf } from '../../../locale';
|
||||
import Icon from '../../../vue_shared/components/icon.vue';
|
||||
import popover from '../../../vue_shared/directives/popover';
|
||||
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
popover,
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scrollTop: 0,
|
||||
isFocused: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allLines() {
|
||||
return this.text.split('\n').map((line, i) => ({
|
||||
text: line.substr(0, this.getLineLength(i)) || ' ',
|
||||
highlightedText: line.substr(this.getLineLength(i)),
|
||||
}));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleScroll() {
|
||||
if (this.$refs.textarea) {
|
||||
this.$nextTick(() => {
|
||||
this.scrollTop = this.$refs.textarea.scrollTop;
|
||||
});
|
||||
}
|
||||
},
|
||||
getLineLength(i) {
|
||||
return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
|
||||
},
|
||||
onInput(e) {
|
||||
this.$emit('input', e.target.value);
|
||||
},
|
||||
updateIsFocused(isFocused) {
|
||||
this.isFocused = isFocused;
|
||||
},
|
||||
},
|
||||
popoverOptions: {
|
||||
trigger: 'hover',
|
||||
placement: 'top',
|
||||
content: sprintf(
|
||||
__(`
|
||||
The character highligher helps you keep the subject line to %{titleLength} characters
|
||||
and wrap the body at %{bodyLength} so they are readable in git.
|
||||
`),
|
||||
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset class="common-note-form ide-commit-message-field">
|
||||
<div
|
||||
class="md-area"
|
||||
:class="{
|
||||
'is-focused': isFocused
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="md-header"
|
||||
>
|
||||
<ul class="nav-links">
|
||||
<li>
|
||||
{{ __('Commit Message') }}
|
||||
<span
|
||||
v-popover="$options.popoverOptions"
|
||||
class="help-block prepend-left-10"
|
||||
>
|
||||
<icon
|
||||
name="question"
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ide-commit-message-textarea-container">
|
||||
<div class="ide-commit-message-highlights-container">
|
||||
<div
|
||||
class="note-textarea highlights monospace"
|
||||
:style="{
|
||||
transform: `translate3d(0, ${-scrollTop}px, 0)`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(line, index) in allLines"
|
||||
:key="index"
|
||||
>
|
||||
<span
|
||||
v-text="line.text"
|
||||
>
|
||||
</span><mark
|
||||
v-show="line.highlightedText"
|
||||
v-text="line.highlightedText"
|
||||
>
|
||||
</mark>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
class="note-textarea ide-commit-message-textarea"
|
||||
name="commit-message"
|
||||
:placeholder="__('Write a commit message...')"
|
||||
:value="text"
|
||||
@scroll="handleScroll"
|
||||
@input="onInput"
|
||||
@focus="updateIsFocused(true)"
|
||||
@blur="updateIsFocused(false)"
|
||||
ref="textarea"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
|
@ -1,82 +1,78 @@
|
|||
<script>
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
checked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
helpText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
computed: {
|
||||
...mapState('commit', [
|
||||
'commitAction',
|
||||
]),
|
||||
...mapGetters('commit', [
|
||||
'newBranchName',
|
||||
]),
|
||||
checked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
methods: {
|
||||
...mapActions('commit', [
|
||||
'updateCommitAction',
|
||||
'updateBranchName',
|
||||
]),
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState('commit', ['commitAction']),
|
||||
...mapGetters('commit', ['newBranchName']),
|
||||
tooltipTitle() {
|
||||
return this.disabled
|
||||
? __('This option is disabled while you still have unstaged changes')
|
||||
: '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset>
|
||||
<label>
|
||||
<label
|
||||
v-tooltip
|
||||
:title="tooltipTitle"
|
||||
:class="{
|
||||
'is-disabled': disabled
|
||||
}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="commit-action"
|
||||
:value="value"
|
||||
@change="updateCommitAction($event.target.value)"
|
||||
:checked="checked"
|
||||
v-once
|
||||
:checked="commitAction === value"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<span class="prepend-left-10">
|
||||
<template v-if="label">
|
||||
{{ label }}
|
||||
</template>
|
||||
<slot v-else></slot>
|
||||
<span
|
||||
v-if="helpText"
|
||||
v-tooltip
|
||||
class="help-block inline"
|
||||
:title="helpText"
|
||||
v-if="label"
|
||||
class="ide-radio-label"
|
||||
>
|
||||
<i
|
||||
class="fa fa-question-circle"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
{{ label }}
|
||||
</span>
|
||||
<slot v-else></slot>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
|
@ -85,7 +81,7 @@
|
|||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
class="form-control monospace"
|
||||
:placeholder="newBranchName"
|
||||
@input="updateBranchName($event.target.value)"
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['stageChange', 'discardFileChanges']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-once
|
||||
class="multi-file-discard-btn"
|
||||
>
|
||||
<button
|
||||
v-tooltip
|
||||
type="button"
|
||||
class="btn btn-blank append-right-5"
|
||||
:aria-label="__('Stage changes')"
|
||||
:title="__('Stage changes')"
|
||||
data-container="body"
|
||||
@click.stop="stageChange(path)"
|
||||
>
|
||||
<icon
|
||||
name="mobile-issue-close"
|
||||
:size="12"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-tooltip
|
||||
type="button"
|
||||
class="btn btn-blank"
|
||||
:aria-label="__('Discard changes')"
|
||||
:title="__('Discard changes')"
|
||||
data-container="body"
|
||||
@click.stop="discardFileChanges(path)"
|
||||
>
|
||||
<icon
|
||||
name="remove"
|
||||
:size="12"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState(['lastCommitMsg', 'committedStateSvgPath']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-panel-success-message"
|
||||
aria-live="assertive"
|
||||
>
|
||||
<div class="svg-content svg-80">
|
||||
<img
|
||||
:src="committedStateSvgPath"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="append-right-default prepend-left-default">
|
||||
<div
|
||||
class="text-content text-center"
|
||||
>
|
||||
<h4>
|
||||
{{ __('All changes are committed') }}
|
||||
</h4>
|
||||
<p v-html="lastCommitMsg"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['unstageChange']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-once
|
||||
class="multi-file-discard-btn"
|
||||
>
|
||||
<button
|
||||
v-tooltip
|
||||
type="button"
|
||||
class="btn btn-blank"
|
||||
:aria-label="__('Unstage changes')"
|
||||
:title="__('Unstage changes')"
|
||||
data-container="body"
|
||||
@click="unstageChange(path)"
|
||||
>
|
||||
<icon
|
||||
name="history"
|
||||
:size="12"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -1,28 +1,15 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { viewerTypes } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
hasChanges: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
mergeRequestId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
viewer: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showShadow: {
|
||||
type: Boolean,
|
||||
mergeRequestId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
@ -38,84 +25,45 @@ export default {
|
|||
this.$emit('click', mode);
|
||||
},
|
||||
},
|
||||
viewerTypes,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dropdown"
|
||||
:class="{
|
||||
shadow: showShadow,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
:class="{
|
||||
'btn-inverted': hasChanges,
|
||||
}"
|
||||
class="btn btn-link"
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<template v-if="viewer === 'mrdiff' && mergeRequestId">
|
||||
{{ mergeReviewLine }}
|
||||
</template>
|
||||
<template v-else-if="viewer === 'editor'">
|
||||
{{ __('Editing') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ __('Reviewing') }}
|
||||
</template>
|
||||
<icon
|
||||
name="angle-down"
|
||||
:size="12"
|
||||
css-classes="caret-down"
|
||||
/>
|
||||
{{ __('Edit') }}
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
|
||||
<ul>
|
||||
<template v-if="mergeRequestId">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="changeMode('mrdiff')"
|
||||
:class="{
|
||||
'is-active': viewer === 'mrdiff',
|
||||
}"
|
||||
>
|
||||
<strong class="dropdown-menu-inner-title">
|
||||
{{ mergeReviewLine }}
|
||||
</strong>
|
||||
<span class="dropdown-menu-inner-content">
|
||||
{{ __('Compare changes with the merge request target branch') }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
role="separator"
|
||||
class="divider"
|
||||
>
|
||||
</li>
|
||||
</template>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="changeMode('editor')"
|
||||
@click.prevent="changeMode($options.viewerTypes.mr)"
|
||||
:class="{
|
||||
'is-active': viewer === 'editor',
|
||||
'is-active': viewer === $options.viewerTypes.mr,
|
||||
}"
|
||||
>
|
||||
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
|
||||
<strong class="dropdown-menu-inner-title">
|
||||
{{ mergeReviewLine }}
|
||||
</strong>
|
||||
<span class="dropdown-menu-inner-content">
|
||||
{{ __('View and edit lines') }}
|
||||
{{ __('Compare changes with the merge request target branch') }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="changeMode('diff')"
|
||||
@click.prevent="changeMode($options.viewerTypes.diff)"
|
||||
:class="{
|
||||
'is-active': viewer === 'diff',
|
||||
'is-active': viewer === $options.viewerTypes.diff,
|
||||
}"
|
||||
>
|
||||
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
|
||||
|
|
243
app/assets/javascripts/ide/components/file_finder/index.vue
Normal file
243
app/assets/javascripts/ide/components/file_finder/index.vue
Normal file
|
@ -0,0 +1,243 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import VirtualList from 'vue-virtual-scroll-list';
|
||||
import Item from './item.vue';
|
||||
import router from '../../ide_router';
|
||||
import {
|
||||
MAX_FILE_FINDER_RESULTS,
|
||||
FILE_FINDER_ROW_HEIGHT,
|
||||
FILE_FINDER_EMPTY_ROW_HEIGHT,
|
||||
} from '../../constants';
|
||||
import {
|
||||
UP_KEY_CODE,
|
||||
DOWN_KEY_CODE,
|
||||
ENTER_KEY_CODE,
|
||||
ESC_KEY_CODE,
|
||||
} from '../../../lib/utils/keycodes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Item,
|
||||
VirtualList,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
focusedIndex: 0,
|
||||
searchText: '',
|
||||
mouseOver: false,
|
||||
cancelMouseOver: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['allBlobs']),
|
||||
...mapState(['fileFindVisible', 'loading']),
|
||||
filteredBlobs() {
|
||||
const searchText = this.searchText.trim();
|
||||
|
||||
if (searchText === '') {
|
||||
return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
|
||||
}
|
||||
|
||||
return fuzzaldrinPlus.filter(this.allBlobs, searchText, {
|
||||
key: 'path',
|
||||
maxResults: MAX_FILE_FINDER_RESULTS,
|
||||
});
|
||||
},
|
||||
filteredBlobsLength() {
|
||||
return this.filteredBlobs.length;
|
||||
},
|
||||
listShowCount() {
|
||||
return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
|
||||
},
|
||||
listHeight() {
|
||||
return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
|
||||
},
|
||||
showClearInputButton() {
|
||||
return this.searchText.trim() !== '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
fileFindVisible() {
|
||||
this.$nextTick(() => {
|
||||
if (!this.fileFindVisible) {
|
||||
this.searchText = '';
|
||||
} else {
|
||||
this.focusedIndex = 0;
|
||||
|
||||
if (this.$refs.searchInput) {
|
||||
this.$refs.searchInput.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
searchText() {
|
||||
this.focusedIndex = 0;
|
||||
},
|
||||
focusedIndex() {
|
||||
if (!this.mouseOver) {
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs.virtualScrollList.$el;
|
||||
const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
|
||||
const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
|
||||
|
||||
if (this.focusedIndex === 0) {
|
||||
// if index is the first index, scroll straight to start
|
||||
el.scrollTop = 0;
|
||||
} else if (this.focusedIndex === this.filteredBlobsLength - 1) {
|
||||
// if index is the last index, scroll to the end
|
||||
el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
|
||||
} else if (scrollTop >= bottom + el.scrollTop) {
|
||||
// if element is off the bottom of the scroll list, scroll down one item
|
||||
el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
|
||||
} else if (scrollTop < el.scrollTop) {
|
||||
// if element is off the top of the scroll list, scroll up one item
|
||||
el.scrollTop = scrollTop;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleFileFinder']),
|
||||
clearSearchInput() {
|
||||
this.searchText = '';
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.searchInput.focus();
|
||||
});
|
||||
},
|
||||
onKeydown(e) {
|
||||
switch (e.keyCode) {
|
||||
case UP_KEY_CODE:
|
||||
e.preventDefault();
|
||||
this.mouseOver = false;
|
||||
this.cancelMouseOver = true;
|
||||
if (this.focusedIndex > 0) {
|
||||
this.focusedIndex -= 1;
|
||||
} else {
|
||||
this.focusedIndex = this.filteredBlobsLength - 1;
|
||||
}
|
||||
break;
|
||||
case DOWN_KEY_CODE:
|
||||
e.preventDefault();
|
||||
this.mouseOver = false;
|
||||
this.cancelMouseOver = true;
|
||||
if (this.focusedIndex < this.filteredBlobsLength - 1) {
|
||||
this.focusedIndex += 1;
|
||||
} else {
|
||||
this.focusedIndex = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
onKeyup(e) {
|
||||
switch (e.keyCode) {
|
||||
case ENTER_KEY_CODE:
|
||||
this.openFile(this.filteredBlobs[this.focusedIndex]);
|
||||
break;
|
||||
case ESC_KEY_CODE:
|
||||
this.toggleFileFinder(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
openFile(file) {
|
||||
this.toggleFileFinder(false);
|
||||
router.push(`/project${file.url}`);
|
||||
},
|
||||
onMouseOver(index) {
|
||||
if (!this.cancelMouseOver) {
|
||||
this.mouseOver = true;
|
||||
this.focusedIndex = index;
|
||||
}
|
||||
},
|
||||
onMouseMove(index) {
|
||||
this.cancelMouseOver = false;
|
||||
this.onMouseOver(index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-file-finder-overlay"
|
||||
@mousedown.self="toggleFileFinder(false)"
|
||||
>
|
||||
<div
|
||||
class="dropdown-menu diff-file-changes ide-file-finder show"
|
||||
>
|
||||
<div class="dropdown-input">
|
||||
<input
|
||||
type="search"
|
||||
class="dropdown-input-field"
|
||||
:placeholder="__('Search files')"
|
||||
autocomplete="off"
|
||||
v-model="searchText"
|
||||
ref="searchInput"
|
||||
@keydown="onKeydown($event)"
|
||||
@keyup="onKeyup($event)"
|
||||
/>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-search dropdown-input-search"
|
||||
:class="{
|
||||
hidden: showClearInputButton
|
||||
}"
|
||||
></i>
|
||||
<i
|
||||
role="button"
|
||||
:aria-label="__('Clear search input')"
|
||||
class="fa fa-times dropdown-input-clear"
|
||||
:class="{
|
||||
show: showClearInputButton
|
||||
}"
|
||||
@click="clearSearchInput"
|
||||
></i>
|
||||
</div>
|
||||
<div>
|
||||
<virtual-list
|
||||
:size="listHeight"
|
||||
:remain="listShowCount"
|
||||
wtag="ul"
|
||||
ref="virtualScrollList"
|
||||
>
|
||||
<template v-if="filteredBlobsLength">
|
||||
<li
|
||||
v-for="(file, index) in filteredBlobs"
|
||||
:key="file.key"
|
||||
>
|
||||
<item
|
||||
class="disable-hover"
|
||||
:file="file"
|
||||
:search-text="searchText"
|
||||
:focused="index === focusedIndex"
|
||||
:index="index"
|
||||
@click="openFile"
|
||||
@mouseover="onMouseOver"
|
||||
@mousemove="onMouseMove"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
<li
|
||||
v-else
|
||||
class="dropdown-menu-empty-item"
|
||||
>
|
||||
<div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
|
||||
<template v-if="loading">
|
||||
{{ __('Loading...') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ __('No files found.') }}
|
||||
</template>
|
||||
</div>
|
||||
</li>
|
||||
</virtual-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
113
app/assets/javascripts/ide/components/file_finder/item.vue
Normal file
113
app/assets/javascripts/ide/components/file_finder/item.vue
Normal file
|
@ -0,0 +1,113 @@
|
|||
<script>
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import FileIcon from '../../../vue_shared/components/file_icon.vue';
|
||||
import ChangedFileIcon from '../changed_file_icon.vue';
|
||||
|
||||
const MAX_PATH_LENGTH = 60;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChangedFileIcon,
|
||||
FileIcon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
focused: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
searchText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pathWithEllipsis() {
|
||||
const path = this.file.path;
|
||||
|
||||
return path.length < MAX_PATH_LENGTH
|
||||
? path
|
||||
: `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
|
||||
},
|
||||
nameSearchTextOccurences() {
|
||||
return fuzzaldrinPlus.match(this.file.name, this.searchText);
|
||||
},
|
||||
pathSearchTextOccurences() {
|
||||
return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickRow() {
|
||||
this.$emit('click', this.file);
|
||||
},
|
||||
mouseOverRow() {
|
||||
this.$emit('mouseover', this.index);
|
||||
},
|
||||
mouseMove() {
|
||||
this.$emit('mousemove', this.index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="diff-changed-file"
|
||||
:class="{
|
||||
'is-focused': focused,
|
||||
}"
|
||||
@click.prevent="clickRow"
|
||||
@mouseover="mouseOverRow"
|
||||
@mousemove="mouseMove"
|
||||
>
|
||||
<file-icon
|
||||
:file-name="file.name"
|
||||
:size="16"
|
||||
css-classes="diff-file-changed-icon append-right-8"
|
||||
/>
|
||||
<span class="diff-changed-file-content append-right-8">
|
||||
<strong
|
||||
class="diff-changed-file-name"
|
||||
>
|
||||
<span
|
||||
v-for="(char, index) in file.name.split('')"
|
||||
:key="index + char"
|
||||
:class="{
|
||||
highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
|
||||
}"
|
||||
v-text="char"
|
||||
>
|
||||
</span>
|
||||
</strong>
|
||||
<span
|
||||
class="diff-changed-file-path prepend-top-5"
|
||||
>
|
||||
<span
|
||||
v-for="(char, index) in pathWithEllipsis.split('')"
|
||||
:key="index + char"
|
||||
:class="{
|
||||
highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
|
||||
}"
|
||||
v-text="char"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="file.changed || file.tempFile"
|
||||
class="diff-changed-stats"
|
||||
>
|
||||
<changed-file-icon
|
||||
:file="file"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
|
@ -1,35 +1,31 @@
|
|||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import ideSidebar from './ide_side_bar.vue';
|
||||
import ideContextbar from './ide_context_bar.vue';
|
||||
import repoTabs from './repo_tabs.vue';
|
||||
import ideStatusBar from './ide_status_bar.vue';
|
||||
import repoEditor from './repo_editor.vue';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import IdeSidebar from './ide_side_bar.vue';
|
||||
import RepoTabs from './repo_tabs.vue';
|
||||
import IdeStatusBar from './ide_status_bar.vue';
|
||||
import RepoEditor from './repo_editor.vue';
|
||||
import FindFile from './file_finder/index.vue';
|
||||
|
||||
const originalStopCallback = Mousetrap.stopCallback;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ideSidebar,
|
||||
ideContextbar,
|
||||
repoTabs,
|
||||
ideStatusBar,
|
||||
repoEditor,
|
||||
},
|
||||
props: {
|
||||
emptyStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noChangesStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
committedStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
IdeSidebar,
|
||||
RepoTabs,
|
||||
IdeStatusBar,
|
||||
RepoEditor,
|
||||
FindFile,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
|
||||
...mapState([
|
||||
'changedFiles',
|
||||
'openFiles',
|
||||
'viewer',
|
||||
'currentMergeRequestId',
|
||||
'fileFindVisible',
|
||||
'emptyStateSvgPath',
|
||||
]),
|
||||
...mapGetters(['activeFile', 'hasChanges']),
|
||||
},
|
||||
mounted() {
|
||||
|
@ -42,67 +38,93 @@ export default {
|
|||
});
|
||||
return returnValue;
|
||||
};
|
||||
|
||||
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
this.toggleFileFinder(!this.fileFindVisible);
|
||||
});
|
||||
|
||||
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleFileFinder']),
|
||||
mousetrapStopCallback(e, el, combo) {
|
||||
if (
|
||||
(combo === 't' && el.classList.contains('dropdown-input-field')) ||
|
||||
el.classList.contains('inputarea')
|
||||
) {
|
||||
return true;
|
||||
} else if (combo === 'command+p' || combo === 'ctrl+p') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return originalStopCallback(e, el, combo);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-view"
|
||||
>
|
||||
<ide-sidebar />
|
||||
<article class="ide">
|
||||
<div
|
||||
class="multi-file-edit-pane"
|
||||
class="ide-view"
|
||||
>
|
||||
<template
|
||||
v-if="activeFile"
|
||||
<find-file
|
||||
v-show="fileFindVisible"
|
||||
/>
|
||||
<ide-sidebar />
|
||||
<div
|
||||
class="multi-file-edit-pane"
|
||||
>
|
||||
<repo-tabs
|
||||
:active-file="activeFile"
|
||||
:files="openFiles"
|
||||
:viewer="viewer"
|
||||
:has-changes="hasChanges"
|
||||
:merge-request-id="currentMergeRequestId"
|
||||
/>
|
||||
<repo-editor
|
||||
class="multi-file-edit-pane-content"
|
||||
:file="activeFile"
|
||||
/>
|
||||
<ide-status-bar
|
||||
:file="activeFile"
|
||||
/>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="ide-empty-state"
|
||||
<template
|
||||
v-if="activeFile"
|
||||
>
|
||||
<div class="row js-empty-state">
|
||||
<div class="col-xs-12">
|
||||
<div class="svg-content svg-250">
|
||||
<img :src="emptyStateSvgPath" />
|
||||
<repo-tabs
|
||||
:active-file="activeFile"
|
||||
:files="openFiles"
|
||||
:viewer="viewer"
|
||||
:has-changes="hasChanges"
|
||||
:merge-request-id="currentMergeRequestId"
|
||||
/>
|
||||
<repo-editor
|
||||
class="multi-file-edit-pane-content"
|
||||
:file="activeFile"
|
||||
/>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="ide-empty-state"
|
||||
>
|
||||
<div class="row js-empty-state">
|
||||
<div class="col-xs-12">
|
||||
<div class="svg-content svg-250">
|
||||
<img :src="emptyStateSvgPath" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="text-content text-center">
|
||||
<h4>
|
||||
Welcome to the GitLab IDE
|
||||
</h4>
|
||||
<p>
|
||||
You can select a file in the left sidebar to begin
|
||||
editing and use the right sidebar to commit your changes.
|
||||
</p>
|
||||
<div class="col-xs-12">
|
||||
<div class="text-content text-center">
|
||||
<h4>
|
||||
Welcome to the GitLab IDE
|
||||
</h4>
|
||||
<p>
|
||||
Select a file from the left sidebar to begin editing.
|
||||
Afterwards, you'll be able to commit your changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<ide-contextbar
|
||||
:no-changes-state-svg-path="noChangesStateSvgPath"
|
||||
:committed-state-svg-path="committedStateSvgPath"
|
||||
<ide-status-bar
|
||||
:file="activeFile"
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import repoCommitSection from './repo_commit_section.vue';
|
||||
import ResizablePanel from './resizable_panel.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
repoCommitSection,
|
||||
icon,
|
||||
panelResizer,
|
||||
ResizablePanel,
|
||||
},
|
||||
props: {
|
||||
noChangesStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
committedStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['changedFiles', 'rightPanelCollapsed']),
|
||||
...mapGetters(['currentIcon']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setPanelCollapsedStatus']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<resizable-panel
|
||||
:collapsible="true"
|
||||
:initial-width="340"
|
||||
side="right"
|
||||
>
|
||||
<div
|
||||
class="multi-file-commit-panel-section"
|
||||
>
|
||||
<header
|
||||
class="multi-file-commit-panel-header"
|
||||
:class="{
|
||||
'is-collapsed': rightPanelCollapsed,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="multi-file-commit-panel-header-title"
|
||||
v-if="!rightPanelCollapsed"
|
||||
>
|
||||
<div
|
||||
v-if="changedFiles.length"
|
||||
>
|
||||
<icon
|
||||
name="list-bulleted"
|
||||
:size="18"
|
||||
/>
|
||||
Staged
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
|
||||
@click.stop="setPanelCollapsedStatus({
|
||||
side: 'right',
|
||||
collapsed: !rightPanelCollapsed,
|
||||
})"
|
||||
>
|
||||
<icon
|
||||
:name="currentIcon"
|
||||
:size="18"
|
||||
/>
|
||||
</button>
|
||||
</header>
|
||||
<repo-commit-section
|
||||
:no-changes-state-svg-path="noChangesStateSvgPath"
|
||||
:committed-state-svg-path="committedStateSvgPath"
|
||||
/>
|
||||
</div>
|
||||
</resizable-panel>
|
||||
</template>
|
|
@ -1,43 +0,0 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
projectUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
goBackUrl() {
|
||||
return document.referrer || this.projectUrl;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
class="ide-external-links"
|
||||
v-once
|
||||
>
|
||||
<p>
|
||||
<a
|
||||
:href="goBackUrl"
|
||||
class="ide-sidebar-link"
|
||||
>
|
||||
<icon
|
||||
:size="16"
|
||||
class="append-right-8"
|
||||
name="go-back"
|
||||
/>
|
||||
<span class="ide-external-links-text">
|
||||
{{ s__('Go back') }}
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
</nav>
|
||||
</template>
|
|
@ -1,47 +0,0 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import repoTree from './ide_repo_tree.vue';
|
||||
import newDropdown from './new_dropdown/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
repoTree,
|
||||
icon,
|
||||
newDropdown,
|
||||
},
|
||||
props: {
|
||||
projectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
branch: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="branch-container">
|
||||
<div class="branch-header">
|
||||
<div class="branch-header-title str-truncated ref-name">
|
||||
<icon
|
||||
name="branch"
|
||||
:size="12"
|
||||
/>
|
||||
{{ branch.name }}
|
||||
</div>
|
||||
<div class="branch-header-btns">
|
||||
<new-dropdown
|
||||
:project-id="projectId"
|
||||
:branch="branch.name"
|
||||
path=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<repo-tree
|
||||
:tree="branch.tree"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,65 +0,0 @@
|
|||
<script>
|
||||
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
|
||||
import Identicon from '../../vue_shared/components/identicon.vue';
|
||||
import BranchesTree from './ide_project_branches_tree.vue';
|
||||
import ExternalLinks from './ide_external_links.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BranchesTree,
|
||||
ExternalLinks,
|
||||
ProjectAvatarImage,
|
||||
Identicon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="projects-sidebar">
|
||||
<div class="context-header">
|
||||
<a
|
||||
:title="project.name"
|
||||
:href="project.web_url"
|
||||
>
|
||||
<div
|
||||
v-if="project.avatar_url"
|
||||
class="avatar-container s40 project-avatar"
|
||||
>
|
||||
<project-avatar-image
|
||||
class="avatar-container project-avatar"
|
||||
:link-href="project.path"
|
||||
:img-src="project.avatar_url"
|
||||
:img-alt="project.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<identicon
|
||||
v-else
|
||||
size-class="s40"
|
||||
:entity-id="project.id"
|
||||
:entity-name="project.name"
|
||||
/>
|
||||
<div class="sidebar-context-title">
|
||||
{{ project.name }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<external-links
|
||||
:project-url="project.web_url"
|
||||
/>
|
||||
<div class="multi-file-commit-panel-inner-scroll">
|
||||
<branches-tree
|
||||
v-for="branch in project.branches"
|
||||
:key="branch.name"
|
||||
:project-id="project.path_with_namespace"
|
||||
:branch="branch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,41 +0,0 @@
|
|||
<script>
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import RepoFile from './repo_file.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RepoFile,
|
||||
SkeletonLoadingContainer,
|
||||
},
|
||||
props: {
|
||||
tree: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-file-list"
|
||||
>
|
||||
<template v-if="tree.loading">
|
||||
<div
|
||||
class="multi-file-loading-container"
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
>
|
||||
<skeleton-loading-container />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<repo-file
|
||||
v-for="file in tree.tree"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
:level="0"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
62
app/assets/javascripts/ide/components/ide_review.vue
Normal file
62
app/assets/javascripts/ide/components/ide_review.vue
Normal file
|
@ -0,0 +1,62 @@
|
|||
<script>
|
||||
import { mapGetters, mapState, mapActions } from 'vuex';
|
||||
import IdeTreeList from './ide_tree_list.vue';
|
||||
import EditorModeDropdown from './editor_mode_dropdown.vue';
|
||||
import { viewerTypes } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IdeTreeList,
|
||||
EditorModeDropdown,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentMergeRequest']),
|
||||
...mapState(['viewer']),
|
||||
showLatestChangesText() {
|
||||
return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
|
||||
},
|
||||
showMergeRequestText() {
|
||||
return this.currentMergeRequest && this.viewer === viewerTypes.mr;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ide-tree-list
|
||||
:viewer-type="viewer"
|
||||
header-class="ide-review-header"
|
||||
:disable-action-dropdown="true"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
>
|
||||
<div class="ide-review-button-holder">
|
||||
{{ __('Review') }}
|
||||
<editor-mode-dropdown
|
||||
v-if="currentMergeRequest"
|
||||
:viewer="viewer"
|
||||
:merge-request-id="currentMergeRequest.iid"
|
||||
@click="updateViewer"
|
||||
/>
|
||||
</div>
|
||||
<div class="prepend-top-5 ide-review-sub-header">
|
||||
<template v-if="showLatestChangesText">
|
||||
{{ __('Latest changes') }}
|
||||
</template>
|
||||
<template v-else-if="showMergeRequestText">
|
||||
{{ __('Merge request') }}
|
||||
(<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</ide-tree-list>
|
||||
</template>
|
|
@ -1,36 +1,82 @@
|
|||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import projectTree from './ide_project_tree.vue';
|
||||
import ResizablePanel from './resizable_panel.vue';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import Identicon from '../../vue_shared/components/identicon.vue';
|
||||
import IdeTree from './ide_tree.vue';
|
||||
import ResizablePanel from './resizable_panel.vue';
|
||||
import ActivityBar from './activity_bar.vue';
|
||||
import CommitSection from './repo_commit_section.vue';
|
||||
import CommitForm from './commit_sidebar/form.vue';
|
||||
import IdeReview from './ide_review.vue';
|
||||
import SuccessMessage from './commit_sidebar/success_message.vue';
|
||||
import { activityBarViews } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
projectTree,
|
||||
icon,
|
||||
panelResizer,
|
||||
skeletonLoadingContainer,
|
||||
ResizablePanel,
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
PanelResizer,
|
||||
SkeletonLoadingContainer,
|
||||
ResizablePanel,
|
||||
ActivityBar,
|
||||
ProjectAvatarImage,
|
||||
Identicon,
|
||||
CommitSection,
|
||||
IdeTree,
|
||||
CommitForm,
|
||||
IdeReview,
|
||||
SuccessMessage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTooltip: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'loading',
|
||||
'currentBranchId',
|
||||
'currentActivityView',
|
||||
'changedFiles',
|
||||
'stagedFiles',
|
||||
'lastCommitMsg',
|
||||
]),
|
||||
...mapGetters(['currentProject', 'someUncommitedChanges']),
|
||||
showSuccessMessage() {
|
||||
return (
|
||||
this.currentActivityView === activityBarViews.edit &&
|
||||
(this.lastCommitMsg && !this.someUncommitedChanges)
|
||||
);
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'loading',
|
||||
]),
|
||||
...mapGetters([
|
||||
'projectsWithTrees',
|
||||
]),
|
||||
branchTooltipTitle() {
|
||||
return this.showTooltip ? this.currentBranchId : undefined;
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
currentBranchId() {
|
||||
this.$nextTick(() => {
|
||||
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<resizable-panel
|
||||
:collapsible="false"
|
||||
:initial-width="290"
|
||||
:initial-width="340"
|
||||
side="left"
|
||||
>
|
||||
<activity-bar
|
||||
v-if="!loading"
|
||||
/>
|
||||
<div class="multi-file-commit-panel-inner">
|
||||
<template v-if="loading">
|
||||
<div
|
||||
|
@ -41,11 +87,54 @@
|
|||
<skeleton-loading-container />
|
||||
</div>
|
||||
</template>
|
||||
<project-tree
|
||||
v-for="project in projectsWithTrees"
|
||||
:key="project.id"
|
||||
:project="project"
|
||||
/>
|
||||
<template v-else>
|
||||
<div class="context-header ide-context-header">
|
||||
<a
|
||||
:href="currentProject.web_url"
|
||||
>
|
||||
<div
|
||||
v-if="currentProject.avatar_url"
|
||||
class="avatar-container s40 project-avatar"
|
||||
>
|
||||
<project-avatar-image
|
||||
class="avatar-container project-avatar"
|
||||
:link-href="currentProject.path"
|
||||
:img-src="currentProject.avatar_url"
|
||||
:img-alt="currentProject.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<identicon
|
||||
v-else
|
||||
size-class="s40"
|
||||
:entity-id="currentProject.id"
|
||||
:entity-name="currentProject.name"
|
||||
/>
|
||||
<div class="ide-sidebar-project-title">
|
||||
<div class="sidebar-context-title">
|
||||
{{ currentProject.name }}
|
||||
</div>
|
||||
<div
|
||||
class="sidebar-context-title ide-sidebar-branch-title"
|
||||
ref="branchId"
|
||||
v-tooltip
|
||||
:title="branchTooltipTitle"
|
||||
>
|
||||
<icon
|
||||
name="branch"
|
||||
css-classes="append-right-5"
|
||||
/>{{ currentBranchId }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="multi-file-commit-panel-inner-scroll">
|
||||
<component
|
||||
:is="currentActivityView"
|
||||
/>
|
||||
</div>
|
||||
<commit-form />
|
||||
</template>
|
||||
</div>
|
||||
</resizable-panel>
|
||||
</template>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
userAvatarImage,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
|
@ -14,47 +17,93 @@ export default {
|
|||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastCommitFormatedAge: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentProject', 'lastCommit']),
|
||||
},
|
||||
mounted() {
|
||||
this.startTimer();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startTimer() {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.commitAgeUpdate();
|
||||
}, 1000);
|
||||
},
|
||||
commitAgeUpdate() {
|
||||
if (this.lastCommit) {
|
||||
this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
|
||||
}
|
||||
},
|
||||
getCommitPath(shortSha) {
|
||||
return `${this.currentProject.web_url}/commit/${shortSha}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-status-bar">
|
||||
<div class="ref-name">
|
||||
<footer class="ide-status-bar">
|
||||
<div
|
||||
class="ide-status-branch"
|
||||
v-if="lastCommit && lastCommitFormatedAge"
|
||||
>
|
||||
<icon
|
||||
name="branch"
|
||||
:size="12"
|
||||
name="commit"
|
||||
/>
|
||||
{{ file.branchId }}
|
||||
<a
|
||||
v-tooltip
|
||||
class="commit-sha"
|
||||
:title="lastCommit.message"
|
||||
:href="getCommitPath(lastCommit.short_id)"
|
||||
>{{ lastCommit.short_id }}</a>
|
||||
by
|
||||
{{ lastCommit.author_name }}
|
||||
<time
|
||||
v-tooltip
|
||||
data-placement="top"
|
||||
data-container="body"
|
||||
:datetime="lastCommit.committed_date"
|
||||
:title="tooltipTitle(lastCommit.committed_date)"
|
||||
>
|
||||
{{ lastCommitFormatedAge }}
|
||||
</time>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="file.lastCommit && file.lastCommit.id">
|
||||
Last commit:
|
||||
<a
|
||||
v-tooltip
|
||||
:title="file.lastCommit.message"
|
||||
:href="file.lastCommit.url"
|
||||
>
|
||||
{{ timeFormated(file.lastCommit.updatedAt) }} by
|
||||
{{ file.lastCommit.author }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div
|
||||
v-if="file"
|
||||
class="ide-status-file"
|
||||
>
|
||||
{{ file.name }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div
|
||||
v-if="file"
|
||||
class="ide-status-file"
|
||||
>
|
||||
{{ file.eol }}
|
||||
</div>
|
||||
<div
|
||||
class="text-right"
|
||||
v-if="!file.binary">
|
||||
class="ide-status-file"
|
||||
v-if="file && !file.binary">
|
||||
{{ file.editorRow }}:{{ file.editorColumn }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div
|
||||
v-if="file"
|
||||
class="ide-status-file"
|
||||
>
|
||||
{{ file.fileLanguage }}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
|
42
app/assets/javascripts/ide/components/ide_tree.vue
Normal file
42
app/assets/javascripts/ide/components/ide_tree.vue
Normal file
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import NewDropdown from './new_dropdown/index.vue';
|
||||
import IdeTreeList from './ide_tree_list.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewDropdown,
|
||||
IdeTreeList,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentBranchId']),
|
||||
...mapGetters(['currentProject', 'currentTree', 'activeFile']),
|
||||
},
|
||||
mounted() {
|
||||
if (this.activeFile && this.activeFile.pending) {
|
||||
this.$router.push(`/project${this.activeFile.url}`, () => {
|
||||
this.updateViewer('editor');
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ide-tree-list
|
||||
viewer-type="editor"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
>
|
||||
{{ __('Edit') }}
|
||||
<new-dropdown
|
||||
:project-id="currentProject.name_with_namespace"
|
||||
:branch="currentBranchId"
|
||||
/>
|
||||
</template>
|
||||
</ide-tree-list>
|
||||
</template>
|
76
app/assets/javascripts/ide/components/ide_tree_list.vue
Normal file
76
app/assets/javascripts/ide/components/ide_tree_list.vue
Normal file
|
@ -0,0 +1,76 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import RepoFile from './repo_file.vue';
|
||||
import NewDropdown from './new_dropdown/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
RepoFile,
|
||||
SkeletonLoadingContainer,
|
||||
NewDropdown,
|
||||
},
|
||||
props: {
|
||||
viewerType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headerClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
disableActionDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentBranchId']),
|
||||
...mapGetters(['currentProject', 'currentTree']),
|
||||
showLoading() {
|
||||
return !this.currentTree || this.currentTree.loading;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateViewer(this.viewerType);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-file-list"
|
||||
>
|
||||
<template v-if="showLoading">
|
||||
<div
|
||||
class="multi-file-loading-container"
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
>
|
||||
<skeleton-loading-container />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<header
|
||||
class="ide-tree-header"
|
||||
:class="headerClass"
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<repo-file
|
||||
v-for="file in currentTree.tree"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
:level="0"
|
||||
:disable-action-dropdown="disableActionDropdown"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -16,8 +16,8 @@ export default {
|
|||
<icon
|
||||
name="git-merge"
|
||||
v-tooltip
|
||||
title="__('Part of merge request changes')"
|
||||
css-classes="ide-file-changed-icon"
|
||||
:title="__('Part of merge request changes')"
|
||||
css-classes="append-right-8"
|
||||
:size="12"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -1,49 +1,55 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import newModal from './modal.vue';
|
||||
import upload from './upload.vue';
|
||||
import { mapActions } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import newModal from './modal.vue';
|
||||
import upload from './upload.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
newModal,
|
||||
upload,
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
newModal,
|
||||
upload,
|
||||
},
|
||||
props: {
|
||||
branch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
branch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openModal: false,
|
||||
modalType: '',
|
||||
dropdownOpen: false,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openModal: false,
|
||||
modalType: '',
|
||||
dropdownOpen: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
dropdownOpen() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dropdownMenu.scrollIntoView();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'createTempEntry',
|
||||
]),
|
||||
createNewItem(type) {
|
||||
this.modalType = type;
|
||||
this.openModal = true;
|
||||
this.dropdownOpen = false;
|
||||
},
|
||||
hideModal() {
|
||||
this.openModal = false;
|
||||
},
|
||||
openDropdown() {
|
||||
this.dropdownOpen = !this.dropdownOpen;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['createTempEntry']),
|
||||
createNewItem(type) {
|
||||
this.modalType = type;
|
||||
this.openModal = true;
|
||||
this.dropdownOpen = false;
|
||||
},
|
||||
};
|
||||
hideModal() {
|
||||
this.openModal = false;
|
||||
},
|
||||
openDropdown() {
|
||||
this.dropdownOpen = !this.dropdownOpen;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -71,7 +77,10 @@
|
|||
css-classes="pull-left"
|
||||
/>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-right"
|
||||
ref="dropdownMenu"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
|
|
|
@ -40,13 +40,6 @@ export default {
|
|||
|
||||
return __('Create file');
|
||||
},
|
||||
formLabelName() {
|
||||
if (this.type === 'tree') {
|
||||
return __('Directory name');
|
||||
}
|
||||
|
||||
return __('File name');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.fieldName.focus();
|
||||
|
@ -82,8 +75,8 @@ export default {
|
|||
@submit.prevent="createEntryInStore"
|
||||
>
|
||||
<fieldset class="form-group append-bottom-0">
|
||||
<label class="label-light col-sm-3">
|
||||
{{ formLabelName }}
|
||||
<label class="label-light col-sm-3 ide-new-modal-label">
|
||||
{{ __('Name') }}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
|
|
|
@ -1,72 +1,65 @@
|
|||
<script>
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
import LoadingButton from '~/vue_shared/components/loading_button.vue';
|
||||
import commitFilesList from './commit_sidebar/list.vue';
|
||||
import CommitFilesList from './commit_sidebar/list.vue';
|
||||
import EmptyState from './commit_sidebar/empty_state.vue';
|
||||
import * as consts from '../stores/modules/commit/constants';
|
||||
import Actions from './commit_sidebar/actions.vue';
|
||||
import { activityBarViews } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DeprecatedModal,
|
||||
icon,
|
||||
commitFilesList,
|
||||
Actions,
|
||||
LoadingButton,
|
||||
Icon,
|
||||
CommitFilesList,
|
||||
EmptyState,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
noChangesStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
committedStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentProjectId',
|
||||
'currentBranchId',
|
||||
'changedFiles',
|
||||
'stagedFiles',
|
||||
'rightPanelCollapsed',
|
||||
'lastCommitMsg',
|
||||
'changedFiles',
|
||||
'unusedSeal',
|
||||
]),
|
||||
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
|
||||
...mapGetters('commit', [
|
||||
'commitButtonDisabled',
|
||||
'discardDraftButtonDisabled',
|
||||
'branchName',
|
||||
]),
|
||||
statusSvg() {
|
||||
return this.lastCommitMsg
|
||||
? this.committedStateSvgPath
|
||||
: this.noChangesStateSvgPath;
|
||||
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
|
||||
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
|
||||
showStageUnstageArea() {
|
||||
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setPanelCollapsedStatus']),
|
||||
...mapActions('commit', [
|
||||
'updateCommitMessage',
|
||||
'discardDraft',
|
||||
'commitChanges',
|
||||
'updateCommitAction',
|
||||
]),
|
||||
toggleCollapsed() {
|
||||
this.setPanelCollapsedStatus({
|
||||
side: 'right',
|
||||
collapsed: !this.rightPanelCollapsed,
|
||||
});
|
||||
watch: {
|
||||
hasChanges() {
|
||||
if (!this.hasChanges) {
|
||||
this.updateActivityBarView(activityBarViews.edit);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.lastOpenedFile) {
|
||||
this.openPendingTab({
|
||||
file: this.lastOpenedFile,
|
||||
})
|
||||
.then(changeViewer => {
|
||||
if (changeViewer) {
|
||||
this.updateViewer('diff');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
|
||||
...mapActions('commit', ['commitChanges', 'updateCommitAction']),
|
||||
forceCreateNewBranch() {
|
||||
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
|
||||
this.commitChanges(),
|
||||
);
|
||||
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -75,9 +68,6 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
class="multi-file-commit-panel-section"
|
||||
:class="{
|
||||
'multi-file-commit-empty-state-container': !changedFiles.length
|
||||
}"
|
||||
>
|
||||
<deprecated-modal
|
||||
id="ide-create-branch-modal"
|
||||
|
@ -91,82 +81,30 @@ export default {
|
|||
Would you like to create a new branch?`) }}
|
||||
</template>
|
||||
</deprecated-modal>
|
||||
<commit-files-list
|
||||
title="Staged"
|
||||
:file-list="changedFiles"
|
||||
:collapsed="rightPanelCollapsed"
|
||||
@toggleCollapsed="toggleCollapsed"
|
||||
/>
|
||||
<template
|
||||
v-if="changedFiles.length"
|
||||
v-if="showStageUnstageArea"
|
||||
>
|
||||
<form
|
||||
class="form-horizontal multi-file-commit-form"
|
||||
@submit.prevent.stop="commitChanges"
|
||||
v-if="!rightPanelCollapsed"
|
||||
>
|
||||
<div class="multi-file-commit-fieldset">
|
||||
<textarea
|
||||
class="form-control multi-file-commit-message"
|
||||
name="commit-message"
|
||||
:value="commitMessage"
|
||||
:placeholder="__('Write a commit message...')"
|
||||
@input="updateCommitMessage($event.target.value)"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="clearfix prepend-top-15">
|
||||
<actions />
|
||||
<loading-button
|
||||
:loading="submitCommitLoading"
|
||||
:disabled="commitButtonDisabled"
|
||||
container-class="btn btn-success btn-sm pull-left"
|
||||
:label="__('Commit')"
|
||||
@click="commitChanges"
|
||||
/>
|
||||
<button
|
||||
v-if="!discardDraftButtonDisabled"
|
||||
type="button"
|
||||
class="btn btn-default btn-sm pull-right"
|
||||
@click="discardDraft"
|
||||
>
|
||||
{{ __('Discard draft') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<commit-files-list
|
||||
class="is-first"
|
||||
icon-name="unstaged"
|
||||
:title="__('Unstaged')"
|
||||
:file-list="changedFiles"
|
||||
action="stageAllChanges"
|
||||
:action-btn-text="__('Stage all')"
|
||||
item-action-component="stage-button"
|
||||
/>
|
||||
<commit-files-list
|
||||
icon-name="staged"
|
||||
:title="__('Staged')"
|
||||
:file-list="stagedFiles"
|
||||
action="unstageAllChanges"
|
||||
:action-btn-text="__('Unstage all')"
|
||||
item-action-component="unstage-button"
|
||||
:staged-list="true"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
v-else-if="!rightPanelCollapsed"
|
||||
class="row js-empty-state"
|
||||
>
|
||||
<div class="col-xs-10 col-xs-offset-1">
|
||||
<div class="svg-content svg-80">
|
||||
<img :src="statusSvg" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-10 col-xs-offset-1">
|
||||
<div
|
||||
class="text-content text-center"
|
||||
v-if="!lastCommitMsg"
|
||||
>
|
||||
<h4>
|
||||
{{ __('No changes') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ __('Edit files in the editor and commit changes here') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="text-content text-center"
|
||||
v-else
|
||||
>
|
||||
<h4>
|
||||
{{ __('All changes are committed') }}
|
||||
</h4>
|
||||
<p v-html="lastCommitMsg">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<empty-state
|
||||
v-if="unusedSeal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import flash from '~/flash';
|
||||
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
|
||||
import { activityBarViews, viewerTypes } from '../constants';
|
||||
import monacoLoader from '../monaco_loader';
|
||||
import Editor from '../lib/editor';
|
||||
import IdeFileButtons from './ide_file_buttons.vue';
|
||||
|
@ -19,8 +20,14 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
|
||||
...mapGetters(['currentMergeRequest']),
|
||||
...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
|
||||
...mapGetters([
|
||||
'currentMergeRequest',
|
||||
'getStagedFile',
|
||||
'isEditModeActive',
|
||||
'isCommitModeActive',
|
||||
'isReviewModeActive',
|
||||
]),
|
||||
shouldHideEditor() {
|
||||
return this.file && this.file.binary && !this.file.content;
|
||||
},
|
||||
|
@ -36,10 +43,29 @@ export default {
|
|||
},
|
||||
},
|
||||
watch: {
|
||||
file(oldVal, newVal) {
|
||||
file(newVal, oldVal) {
|
||||
if (oldVal.pending) {
|
||||
this.removePendingTab(oldVal);
|
||||
}
|
||||
|
||||
// Compare key to allow for files opened in review mode to be cached differently
|
||||
if (newVal.key !== this.file.key) {
|
||||
if (oldVal.key !== this.file.key) {
|
||||
this.initMonaco();
|
||||
|
||||
if (this.currentActivityView !== activityBarViews.edit) {
|
||||
this.setFileViewMode({
|
||||
file: this.file,
|
||||
viewMode: 'edit',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
currentActivityView() {
|
||||
if (this.currentActivityView !== activityBarViews.edit) {
|
||||
this.setFileViewMode({
|
||||
file: this.file,
|
||||
viewMode: 'edit',
|
||||
});
|
||||
}
|
||||
},
|
||||
rightPanelCollapsed() {
|
||||
|
@ -77,7 +103,7 @@ export default {
|
|||
'setFileViewMode',
|
||||
'setFileEOL',
|
||||
'updateViewer',
|
||||
'updateDelayViewerUpdated',
|
||||
'removePendingTab',
|
||||
]),
|
||||
initMonaco() {
|
||||
if (this.shouldHideEditor) return;
|
||||
|
@ -89,14 +115,6 @@ export default {
|
|||
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
|
||||
})
|
||||
.then(() => {
|
||||
const viewerPromise = this.delayViewerUpdated
|
||||
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
|
||||
: Promise.resolve();
|
||||
|
||||
return viewerPromise;
|
||||
})
|
||||
.then(() => {
|
||||
this.updateDelayViewerUpdated(false);
|
||||
this.createEditorInstance();
|
||||
})
|
||||
.catch(err => {
|
||||
|
@ -108,10 +126,10 @@ export default {
|
|||
this.editor.dispose();
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.viewer === 'editor') {
|
||||
if (this.viewer === viewerTypes.edit) {
|
||||
this.editor.createInstance(this.$refs.editor);
|
||||
} else {
|
||||
this.editor.createDiffInstance(this.$refs.editor);
|
||||
this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
|
||||
}
|
||||
|
||||
this.setupEditor();
|
||||
|
@ -120,9 +138,14 @@ export default {
|
|||
setupEditor() {
|
||||
if (!this.file || !this.editor.instance) return;
|
||||
|
||||
this.model = this.editor.createModel(this.file);
|
||||
const head = this.getStagedFile(this.file.path);
|
||||
|
||||
if (this.viewer === 'mrdiff') {
|
||||
this.model = this.editor.createModel(
|
||||
this.file,
|
||||
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
|
||||
);
|
||||
|
||||
if (this.viewer === viewerTypes.mr && this.file.mrChange) {
|
||||
this.editor.attachMergeRequestModel(this.model);
|
||||
} else {
|
||||
this.editor.attachModel(this.model);
|
||||
|
@ -163,6 +186,7 @@ export default {
|
|||
});
|
||||
},
|
||||
},
|
||||
viewerTypes,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -171,16 +195,17 @@ export default {
|
|||
id="ide"
|
||||
class="blob-viewer-container blob-editor-container"
|
||||
>
|
||||
<div class="ide-mode-tabs clearfix">
|
||||
<div class="ide-mode-tabs clearfix" >
|
||||
<ul
|
||||
class="nav-links pull-left"
|
||||
v-if="!shouldHideEditor">
|
||||
v-if="!shouldHideEditor && isEditModeActive"
|
||||
>
|
||||
<li :class="editTabCSS">
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
role="button"
|
||||
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
|
||||
<template v-if="viewer === 'editor'">
|
||||
<template v-if="viewer === $options.viewerTypes.edit">
|
||||
{{ __('Edit') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
|
@ -207,6 +232,9 @@ export default {
|
|||
v-show="!shouldHideEditor && file.viewMode === 'edit'"
|
||||
ref="editor"
|
||||
class="multi-file-editor-holder"
|
||||
:class="{
|
||||
'is-readonly': isCommitModeActive,
|
||||
}"
|
||||
>
|
||||
</div>
|
||||
<content-viewer
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import fileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { n__, __, sprintf } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import router from '../ide_router';
|
||||
import newDropdown from './new_dropdown/index.vue';
|
||||
import fileStatusIcon from './repo_file_status_icon.vue';
|
||||
import changedFileIcon from './changed_file_icon.vue';
|
||||
import mrFileIcon from './mr_file_icon.vue';
|
||||
import NewDropdown from './new_dropdown/index.vue';
|
||||
import FileStatusIcon from './repo_file_status_icon.vue';
|
||||
import ChangedFileIcon from './changed_file_icon.vue';
|
||||
import MrFileIcon from './mr_file_icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepoFile',
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
skeletonLoadingContainer,
|
||||
newDropdown,
|
||||
fileStatusIcon,
|
||||
fileIcon,
|
||||
changedFileIcon,
|
||||
mrFileIcon,
|
||||
SkeletonLoadingContainer,
|
||||
NewDropdown,
|
||||
FileStatusIcon,
|
||||
FileIcon,
|
||||
ChangedFileIcon,
|
||||
MrFileIcon,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
|
@ -27,8 +34,41 @@ export default {
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
disableActionDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getChangesInFolder',
|
||||
'getUnstagedFilesCountForPath',
|
||||
'getStagedFilesCountForPath',
|
||||
]),
|
||||
folderUnstagedCount() {
|
||||
return this.getUnstagedFilesCountForPath(this.file.path);
|
||||
},
|
||||
folderStagedCount() {
|
||||
return this.getStagedFilesCountForPath(this.file.path);
|
||||
},
|
||||
changesCount() {
|
||||
return this.getChangesInFolder(this.file.path);
|
||||
},
|
||||
folderChangesTooltip() {
|
||||
if (this.changesCount === 0) return undefined;
|
||||
|
||||
if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
|
||||
return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
|
||||
} else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
|
||||
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
|
||||
}
|
||||
|
||||
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
|
||||
unstaged: this.folderUnstagedCount,
|
||||
staged: this.folderStagedCount,
|
||||
});
|
||||
},
|
||||
isTree() {
|
||||
return this.file.type === 'tree';
|
||||
},
|
||||
|
@ -48,23 +88,30 @@ export default {
|
|||
'is-open': this.file.opened,
|
||||
};
|
||||
},
|
||||
showTreeChangesCount() {
|
||||
return this.isTree && this.changesCount > 0 && !this.file.opened;
|
||||
},
|
||||
showChangedFileIcon() {
|
||||
return this.file.changed || this.file.tempFile || this.file.staged;
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
if (this.file.type === 'blob' && this.file.active) {
|
||||
this.$el.scrollIntoView();
|
||||
this.$el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
|
||||
...mapActions(['toggleTreeOpen']),
|
||||
clickFile() {
|
||||
// Manual Action if a tree is selected/opened
|
||||
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
|
||||
this.toggleTreeOpen(this.file.path);
|
||||
}
|
||||
|
||||
return this.updateDelayViewerUpdated(true).then(() => {
|
||||
router.push(`/project${this.file.url}`);
|
||||
});
|
||||
router.push(`/project${this.file.url}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -97,17 +144,36 @@ export default {
|
|||
:file="file"
|
||||
/>
|
||||
</span>
|
||||
<span class="pull-right">
|
||||
<span class="pull-right ide-file-icon-holder">
|
||||
<mr-file-icon
|
||||
v-if="file.mrChange"
|
||||
/>
|
||||
<span
|
||||
v-if="showTreeChangesCount"
|
||||
class="ide-tree-changes"
|
||||
>
|
||||
{{ changesCount }}
|
||||
<icon
|
||||
v-tooltip
|
||||
:title="folderChangesTooltip"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
name="file-modified"
|
||||
:size="12"
|
||||
css-classes="prepend-left-5 multi-file-modified"
|
||||
/>
|
||||
</span>
|
||||
<changed-file-icon
|
||||
v-else-if="showChangedFileIcon"
|
||||
:file="file"
|
||||
v-if="file.changed || file.tempFile"
|
||||
:show-tooltip="true"
|
||||
:show-staged-icon="true"
|
||||
:force-modified-icon="true"
|
||||
class="pull-right"
|
||||
/>
|
||||
</span>
|
||||
<new-dropdown
|
||||
v-if="isTree"
|
||||
v-if="isTree && !disableActionDropdown"
|
||||
:project-id="file.projectId"
|
||||
:branch="file.branchId"
|
||||
:path="file.path"
|
||||
|
|
|
@ -26,13 +26,18 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
closeLabel() {
|
||||
if (this.tab.changed || this.tab.tempFile) {
|
||||
if (this.fileHasChanged) {
|
||||
return `${this.tab.name} changed`;
|
||||
}
|
||||
return `Close ${this.tab.name}`;
|
||||
},
|
||||
showChangedIcon() {
|
||||
return this.tab.changed ? !this.tabMouseOver : false;
|
||||
if (this.tab.pending) return true;
|
||||
|
||||
return this.fileHasChanged ? !this.tabMouseOver : false;
|
||||
},
|
||||
fileHasChanged() {
|
||||
return this.tab.changed || this.tab.tempFile || this.tab.staged;
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -42,18 +47,18 @@ export default {
|
|||
this.updateDelayViewerUpdated(true);
|
||||
|
||||
if (tab.pending) {
|
||||
this.openPendingTab(tab);
|
||||
this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
|
||||
} else {
|
||||
this.$router.push(`/project${tab.url}`);
|
||||
}
|
||||
},
|
||||
mouseOverTab() {
|
||||
if (this.tab.changed) {
|
||||
if (this.fileHasChanged) {
|
||||
this.tabMouseOver = true;
|
||||
}
|
||||
},
|
||||
mouseOutTab() {
|
||||
if (this.tab.changed) {
|
||||
if (this.fileHasChanged) {
|
||||
this.tabMouseOver = false;
|
||||
}
|
||||
},
|
||||
|
@ -63,32 +68,15 @@ export default {
|
|||
|
||||
<template>
|
||||
<li
|
||||
:class="{
|
||||
active: tab.active
|
||||
}"
|
||||
@click="clickFile(tab)"
|
||||
@mouseover="mouseOverTab"
|
||||
@mouseout="mouseOutTab"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="multi-file-tab-close"
|
||||
@click.stop.prevent="closeFile(tab)"
|
||||
:aria-label="closeLabel"
|
||||
>
|
||||
<icon
|
||||
v-if="!showChangedIcon"
|
||||
name="close"
|
||||
:size="12"
|
||||
/>
|
||||
<changed-file-icon
|
||||
v-else
|
||||
:file="tab"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="multi-file-tab"
|
||||
:class="{
|
||||
active: tab.active
|
||||
}"
|
||||
:title="tab.url"
|
||||
>
|
||||
<file-icon
|
||||
|
@ -100,5 +88,23 @@ export default {
|
|||
:file="tab"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="multi-file-tab-close"
|
||||
@click.stop.prevent="closeFile(tab)"
|
||||
:aria-label="closeLabel"
|
||||
:disabled="tab.pending"
|
||||
>
|
||||
<icon
|
||||
v-if="!showChangedIcon"
|
||||
name="close"
|
||||
:size="12"
|
||||
/>
|
||||
<changed-file-icon
|
||||
v-else
|
||||
:file="tab"
|
||||
:force-modified-icon="true"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
@ -32,16 +32,6 @@ export default {
|
|||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showShadow: false,
|
||||
};
|
||||
},
|
||||
updated() {
|
||||
if (!this.$refs.tabsScroller) return;
|
||||
|
||||
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer', 'removePendingTab']),
|
||||
openFileViewer(viewer) {
|
||||
|
@ -71,12 +61,5 @@ export default {
|
|||
:tab="tab"
|
||||
/>
|
||||
</ul>
|
||||
<editor-mode
|
||||
:viewer="viewer"
|
||||
:show-shadow="showShadow"
|
||||
:has-changes="hasChanges"
|
||||
:merge-request-id="mergeRequestId"
|
||||
@click="openFileViewer"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
24
app/assets/javascripts/ide/constants.js
Normal file
24
app/assets/javascripts/ide/constants.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Fuzzy file finder
|
||||
export const MAX_FILE_FINDER_RESULTS = 40;
|
||||
export const FILE_FINDER_ROW_HEIGHT = 55;
|
||||
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
|
||||
|
||||
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
|
||||
|
||||
export const COMMIT_ITEM_PADDING = 32;
|
||||
|
||||
// Commit message textarea
|
||||
export const MAX_TITLE_LENGTH = 50;
|
||||
export const MAX_BODY_LENGTH = 72;
|
||||
|
||||
export const activityBarViews = {
|
||||
edit: 'ide-tree',
|
||||
commit: 'commit-section',
|
||||
review: 'ide-review',
|
||||
};
|
||||
|
||||
export const viewerTypes = {
|
||||
mr: 'mrdiff',
|
||||
edit: 'editor',
|
||||
diff: 'diff',
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue