Merge branch 'master' into 38759-fetch-available-parameters-directly-from-gke-when-creating-a-cluster
This commit is contained in:
commit
a1e1bbef7a
806 changed files with 10461 additions and 5639 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -72,3 +72,4 @@ eslint-report.html
|
|||
/locale/**/*.time_stamp
|
||||
/.rspec
|
||||
/plugins/*
|
||||
/.gitlab_pages_secret
|
||||
|
|
|
@ -110,7 +110,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 +122,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 +226,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 +266,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 +293,6 @@ stages:
|
|||
# Trigger a package build in omnibus-gitlab repository
|
||||
#
|
||||
package-and-qa:
|
||||
<<: *dedicated-runner
|
||||
image: ruby:2.4-alpine
|
||||
before_script: []
|
||||
stage: build
|
||||
|
@ -648,7 +651,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
|
||||
|
@ -671,7 +674,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"
|
||||
|
@ -692,7 +695,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
|
||||
|
@ -816,7 +819,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
|
||||
|
|
|
@ -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.
|
||||
|
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -2,6 +2,42 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 10.7.2 (2018-04-25)
|
||||
|
||||
### Security (2 changes)
|
||||
|
||||
- Serve archive requests with the correct file in all cases.
|
||||
- Sanitizes user name to avoid XSS attacks.
|
||||
|
||||
|
||||
## 10.7.1 (2018-04-23)
|
||||
|
||||
### Fixed (11 changes)
|
||||
|
||||
- [API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors` when no value is passed for `order_by` or `sort`. !18393
|
||||
- Fix a case with secret variables being empty sometimes. !18400
|
||||
- Fix `Trace::HttpIO` can not render multi-byte chars. !18417
|
||||
- Fix specifying a non-default ref when requesting an archive using the legacy URL. !18468
|
||||
- Respect visibility options and description when importing project from template. !18473
|
||||
- Removes 'No Job log' message from build trace. !18523
|
||||
- Align action icons in pipeline graph.
|
||||
- Fix direct_upload when records with null file_store are used.
|
||||
- Removed alert box in IDE when redirecting to new merge request.
|
||||
- Fixed IDE not loading for sub groups.
|
||||
- Fixed IDE not showing loading state when tree is loading.
|
||||
|
||||
### Performance (4 changes)
|
||||
|
||||
- Validate project path prior to hitting the database. !18322
|
||||
- Add index to file_store on ci_job_artifacts. !18444
|
||||
- Fix N+1 queries when loading participants for a commit note.
|
||||
- Support Markdown rendering using multiple projects.
|
||||
|
||||
### Added (1 change)
|
||||
|
||||
- Add an API endpoint to download git repository snapshots. !18173
|
||||
|
||||
|
||||
## 10.7.0 (2018-04-22)
|
||||
|
||||
### Security (6 changes, 2 of them are from the community)
|
||||
|
@ -209,6 +245,13 @@ 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)
|
||||
|
@ -450,6 +493,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)
|
||||
|
|
|
@ -25,10 +25,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)
|
||||
|
@ -112,7 +114,7 @@ is a great place to start. Issues with a lower weight (1 or 2) are deemed
|
|||
suitable for beginners. These issues will be of reasonable size and challenge,
|
||||
for anyone to start contributing to GitLab. If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to
|
||||
learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel
|
||||
please consider we favor
|
||||
please consider we favor
|
||||
[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution!
|
||||
|
||||
## Workflow labels
|
||||
|
@ -125,8 +127,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.
|
||||
- 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 +171,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
|
||||
|
@ -195,12 +199,12 @@ release. There are three levels of Milestone labels:
|
|||
- ~Stretch: Issues that are a stretch goal for delivering in the current
|
||||
milestone. If these issues are not done in the current release, they will
|
||||
strongly be considered for the next release.
|
||||
- ~"Next Patch Release": Issues to put in the next patch release. Work on these
|
||||
- ~"Next Patch Release": Issues to put in the next patch release. Work on these
|
||||
first, and add the "Pick Into X" label to the merge request, along with the
|
||||
appropriate milestone.
|
||||
|
||||
Each issue scheduled for the current milestone should be labeled ~Deliverable
|
||||
or ~"Stretch". Any open issue for a previous milestone should be labeled
|
||||
or ~"Stretch". Any open issue for a previous milestone should be labeled
|
||||
~"Next Patch Release", or otherwise rescheduled to a different milestone.
|
||||
|
||||
### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
|
||||
|
@ -210,23 +214,23 @@ This label documents the planned timeline & urgency which is used to measure aga
|
|||
|
||||
| Label | Meaning | Estimate time to fix | Guidance |
|
||||
|-------|-----------------|------------------------------------------------------------------|----------|
|
||||
| ~P1 | Urgent Priority | The current release | |
|
||||
| ~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 |
|
||||
| Label | Availability / Performance |
|
||||
|-------|--------------------------------------------------------------|
|
||||
| ~P1 | |
|
||||
| ~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.
|
||||
Severity labels help us clearly communicate the impact of a ~bug on users.
|
||||
|
||||
| Label | Meaning | Impact of the defect | Example |
|
||||
|-------|-------------------|-------------------------------------------------------|---------|
|
||||
|
@ -237,9 +241,9 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
|
|||
|
||||
#### Specific Severity guidance
|
||||
|
||||
| Label | Security Impact |
|
||||
| Label | Security Impact |
|
||||
|-------|-------------------------------------------------------------------|
|
||||
| ~S1 | >50% customers impacted (possible company extinction level event) |
|
||||
| ~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 |
|
||||
|
@ -723,4 +727,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.96.2
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.8.0
|
||||
0.9.1
|
||||
|
|
|
@ -1 +1 @@
|
|||
4.1.0
|
||||
4.2.0
|
||||
|
|
7
Gemfile
7
Gemfile
|
@ -51,7 +51,6 @@ gem 'omniauth-shibboleth', '~> 1.2.0'
|
|||
gem 'omniauth-twitter', '~> 1.4'
|
||||
gem 'omniauth_crowd', '~> 2.2.0'
|
||||
gem 'omniauth-authentiq', '~> 0.3.1'
|
||||
gem 'omniauth-jwt', '~> 0.0.2'
|
||||
gem 'rack-oauth2', '~> 1.2.1'
|
||||
gem 'jwt', '~> 1.5.6'
|
||||
|
||||
|
@ -185,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'
|
||||
|
||||
|
@ -283,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'
|
||||
|
@ -416,7 +417,7 @@ end
|
|||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
|
||||
gem 'grpc', '~> 1.10.0'
|
||||
gem 'grpc', '~> 1.11.0'
|
||||
|
||||
# Locked until https://github.com/google/protobuf/issues/4210 is closed
|
||||
gem 'google-protobuf', '= 3.5.1'
|
||||
|
|
24
Gemfile.lock
24
Gemfile.lock
|
@ -161,6 +161,7 @@ GEM
|
|||
activerecord (>= 3.2.0, < 5.1)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
device_detector (1.0.0)
|
||||
devise (4.2.0)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
|
@ -178,7 +179,7 @@ GEM
|
|||
docile (1.1.5)
|
||||
domain_name (0.5.20170404)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (4.3.1)
|
||||
doorkeeper (4.3.2)
|
||||
railties (>= 4.2)
|
||||
doorkeeper-openid_connect (1.3.0)
|
||||
doorkeeper (~> 4.3)
|
||||
|
@ -374,7 +375,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)
|
||||
|
@ -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)
|
||||
|
@ -554,9 +556,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 +601,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
|
||||
|
@ -1030,6 +1027,7 @@ DEPENDENCIES
|
|||
database_cleaner (~> 1.5.0)
|
||||
deckar01-task_list (= 2.0.0)
|
||||
default_value_for (~> 3.0.0)
|
||||
device_detector
|
||||
devise (~> 4.2)
|
||||
devise-two-factor (~> 3.0.0)
|
||||
diffy (~> 3.1.0)
|
||||
|
@ -1077,7 +1075,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
|
||||
|
@ -1118,7 +1116,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)
|
||||
|
@ -1129,7 +1126,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)
|
||||
|
|
|
@ -304,12 +304,12 @@ GEM
|
|||
flowdock (~> 0.7)
|
||||
gitlab-grit (>= 2.4.1)
|
||||
multi_json
|
||||
gitlab-gollum-lib (4.2.7.1)
|
||||
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 (~> 2.1)
|
||||
rouge (~> 3.1)
|
||||
sanitize (~> 2.1)
|
||||
stringex (~> 2.6)
|
||||
gitlab-gollum-rugged_adapter (0.4.4)
|
||||
|
@ -602,8 +602,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
|
||||
|
@ -752,7 +750,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)
|
||||
|
@ -1134,7 +1132,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)
|
||||
|
@ -1166,7 +1163,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)
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
10.7.0-pre
|
||||
10.8.0-pre
|
||||
|
|
|
@ -26,11 +26,18 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
forceModifiedIcon: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
changedIcon() {
|
||||
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
|
||||
return this.file.tempFile ? `file-addition${suffix}` : `file-modified${suffix}`;
|
||||
return this.file.tempFile && !this.forceModifiedIcon
|
||||
? `file-addition${suffix}`
|
||||
: `file-modified${suffix}`;
|
||||
},
|
||||
stagedIcon() {
|
||||
return `${this.changedIcon}-solid`;
|
||||
|
|
245
app/assets/javascripts/ide/components/file_finder/index.vue
Normal file
245
app/assets/javascripts/ide/components/file_finder/index.vue
Normal file
|
@ -0,0 +1,245 @@
|
|||
<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,
|
||||
})
|
||||
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
|
||||
},
|
||||
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,55 +1,91 @@
|
|||
<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 { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import Mousetrap from 'mousetrap';
|
||||
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 FindFile from './file_finder/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ideSidebar,
|
||||
ideContextbar,
|
||||
repoTabs,
|
||||
ideStatusBar,
|
||||
repoEditor,
|
||||
},
|
||||
props: {
|
||||
emptyStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noChangesStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
committedStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
|
||||
...mapGetters(['activeFile', 'hasChanges']),
|
||||
},
|
||||
mounted() {
|
||||
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
||||
window.onbeforeunload = e => {
|
||||
if (!this.changedFiles.length) return undefined;
|
||||
const originalStopCallback = Mousetrap.stopCallback;
|
||||
|
||||
Object.assign(e, {
|
||||
returnValue,
|
||||
export default {
|
||||
components: {
|
||||
ideSidebar,
|
||||
ideContextbar,
|
||||
repoTabs,
|
||||
ideStatusBar,
|
||||
repoEditor,
|
||||
FindFile,
|
||||
},
|
||||
props: {
|
||||
emptyStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noChangesStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
committedStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'changedFiles',
|
||||
'openFiles',
|
||||
'viewer',
|
||||
'currentMergeRequestId',
|
||||
'fileFindVisible',
|
||||
]),
|
||||
...mapGetters(['activeFile', 'hasChanges']),
|
||||
},
|
||||
mounted() {
|
||||
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
||||
window.onbeforeunload = e => {
|
||||
if (!this.changedFiles.length) return undefined;
|
||||
|
||||
Object.assign(e, {
|
||||
returnValue,
|
||||
});
|
||||
return returnValue;
|
||||
};
|
||||
|
||||
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
this.toggleFileFinder(!this.fileFindVisible);
|
||||
});
|
||||
return returnValue;
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
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')) {
|
||||
return true;
|
||||
} else if (combo === 'command+p' || combo === 'ctrl+p') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return originalStopCallback(e, el, combo);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-view"
|
||||
>
|
||||
<find-file
|
||||
v-show="fileFindVisible"
|
||||
/>
|
||||
<ide-sidebar />
|
||||
<div
|
||||
class="multi-file-edit-pane"
|
||||
|
|
|
@ -1,49 +1,54 @@
|
|||
<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: true,
|
||||
},
|
||||
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 +76,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
|
||||
|
|
|
@ -97,7 +97,7 @@ export default {
|
|||
:file="file"
|
||||
/>
|
||||
</span>
|
||||
<span class="pull-right">
|
||||
<span class="pull-right ide-file-icon-holder">
|
||||
<mr-file-icon
|
||||
v-if="file.mrChange"
|
||||
/>
|
||||
|
@ -106,7 +106,8 @@ export default {
|
|||
:file="file"
|
||||
:show-tooltip="true"
|
||||
:show-staged-icon="true"
|
||||
class="prepend-top-5 pull-right"
|
||||
:force-modified-icon="true"
|
||||
class="pull-right"
|
||||
/>
|
||||
</span>
|
||||
<new-dropdown
|
||||
|
|
|
@ -84,6 +84,7 @@ export default {
|
|||
<changed-file-icon
|
||||
v-else
|
||||
:file="tab"
|
||||
:force-modified-icon="true"
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// 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;
|
||||
|
||||
// Commit message textarea
|
||||
export const MAX_TITLE_LENGTH = 50;
|
||||
export const MAX_BODY_LENGTH = 72;
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import _ from 'underscore';
|
||||
import store from '../stores';
|
||||
import DecorationsController from './decorations/controller';
|
||||
import DirtyDiffController from './diff/controller';
|
||||
import Disposable from './common/disposable';
|
||||
import ModelManager from './common/model_manager';
|
||||
import editorOptions, { defaultEditorOptions } from './editor_options';
|
||||
import gitlabTheme from './themes/gl_theme';
|
||||
import keymap from './keymap.json';
|
||||
|
||||
export const clearDomElement = el => {
|
||||
if (!el || !el.firstChild) return;
|
||||
|
@ -53,6 +55,8 @@ export default class Editor {
|
|||
)),
|
||||
);
|
||||
|
||||
this.addCommands();
|
||||
|
||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +77,8 @@ export default class Editor {
|
|||
})),
|
||||
);
|
||||
|
||||
this.addCommands();
|
||||
|
||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||
}
|
||||
}
|
||||
|
@ -189,4 +195,31 @@ export default class Editor {
|
|||
static renderSideBySide(domElement) {
|
||||
return domElement.offsetWidth >= 700;
|
||||
}
|
||||
|
||||
addCommands() {
|
||||
const getKeyCode = key => {
|
||||
const monacoKeyMod = key.indexOf('KEY_') === 0;
|
||||
|
||||
return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
|
||||
};
|
||||
|
||||
keymap.forEach(command => {
|
||||
const keybindings = command.bindings.map(binding => {
|
||||
const keys = binding.split('+');
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
|
||||
});
|
||||
|
||||
this.instance.addAction({
|
||||
id: command.id,
|
||||
label: command.label,
|
||||
keybindings,
|
||||
run() {
|
||||
store.dispatch(command.action.name, command.action.params);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
11
app/assets/javascripts/ide/lib/keymap.json
Normal file
11
app/assets/javascripts/ide/lib/keymap.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
[
|
||||
{
|
||||
"id": "file-finder",
|
||||
"label": "File finder",
|
||||
"bindings": ["CtrlCmd+KEY_P"],
|
||||
"action": {
|
||||
"name": "toggleFileFinder",
|
||||
"params": true
|
||||
}
|
||||
}
|
||||
]
|
|
@ -33,10 +33,7 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const toggleRightPanelCollapsed = (
|
||||
{ dispatch, state },
|
||||
e = undefined,
|
||||
) => {
|
||||
export const toggleRightPanelCollapsed = ({ dispatch, state }, e = undefined) => {
|
||||
if (e) {
|
||||
$(e.currentTarget)
|
||||
.tooltip('hide')
|
||||
|
@ -77,7 +74,7 @@ export const createTempEntry = (
|
|||
}
|
||||
|
||||
worker.addEventListener('message', ({ data }) => {
|
||||
const { file } = data;
|
||||
const { file, parentPath } = data;
|
||||
|
||||
worker.terminate();
|
||||
|
||||
|
@ -93,6 +90,10 @@ export const createTempEntry = (
|
|||
dispatch('setFileActive', file.path);
|
||||
}
|
||||
|
||||
if (parentPath && !state.entries[parentPath].opened) {
|
||||
commit(types.TOGGLE_TREE_OPEN, parentPath);
|
||||
}
|
||||
|
||||
resolve(file);
|
||||
});
|
||||
|
||||
|
@ -137,7 +138,21 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
|
|||
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
|
||||
};
|
||||
|
||||
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
|
||||
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
|
||||
|
||||
if (file.parentPath) {
|
||||
dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile });
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleFileFinder = ({ commit }, fileFindVisible) =>
|
||||
commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
|
||||
|
||||
export * from './actions/tree';
|
||||
export * from './actions/file';
|
||||
export * from './actions/project';
|
||||
export * from './actions/merge_request';
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -63,7 +63,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
|
|||
const file = state.entries[path];
|
||||
commit(types.TOGGLE_LOADING, { entry: file });
|
||||
return service
|
||||
.getFileData(file.url)
|
||||
.getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url}`)
|
||||
.then(res => {
|
||||
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
|
||||
setPageTitle(pageTitle);
|
||||
|
|
|
@ -42,4 +42,20 @@ export const collapseButtonTooltip = state =>
|
|||
|
||||
export const hasMergeRequest = state => !!state.currentMergeRequestId;
|
||||
|
||||
export const allBlobs = state =>
|
||||
Object.keys(state.entries)
|
||||
.reduce((acc, key) => {
|
||||
const entry = state.entries[key];
|
||||
|
||||
if (entry.type === 'blob') {
|
||||
acc.push(entry);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
|
||||
|
||||
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -110,6 +110,17 @@ export const updateFilesAfterCommit = (
|
|||
{ root: true },
|
||||
);
|
||||
|
||||
commit(
|
||||
rootTypes.TOGGLE_FILE_CHANGED,
|
||||
{
|
||||
file,
|
||||
changed: false,
|
||||
},
|
||||
{ root: true },
|
||||
);
|
||||
|
||||
dispatch('updateTempFlagForEntry', { file, tempFile: false }, { root: true });
|
||||
|
||||
eventHub.$emit(`editor.update.model.content.${file.key}`, {
|
||||
content: file.content,
|
||||
changed: !!changedFile,
|
||||
|
@ -185,3 +196,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
|
|||
commit(types.UPDATE_LOADING, false);
|
||||
});
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -27,3 +27,6 @@ export const branchName = (state, getters, rootState) => {
|
|||
|
||||
return rootState.currentBranchId;
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -58,3 +58,6 @@ export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
|
|||
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
|
||||
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
|
||||
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
|
||||
|
||||
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
|
||||
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
|
||||
|
|
|
@ -4,6 +4,7 @@ import mergeRequestMutation from './mutations/merge_request';
|
|||
import fileMutations from './mutations/file';
|
||||
import treeMutations from './mutations/tree';
|
||||
import branchMutations from './mutations/branch';
|
||||
import { sortTree } from './utils';
|
||||
|
||||
export default {
|
||||
[types.SET_INITIAL_DATA](state, data) {
|
||||
|
@ -73,7 +74,7 @@ export default {
|
|||
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
|
||||
);
|
||||
Object.assign(foundEntry, {
|
||||
tree: foundEntry.tree.concat(tree),
|
||||
tree: sortTree(foundEntry.tree.concat(tree)),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -86,10 +87,16 @@ export default {
|
|||
|
||||
if (!foundEntry) {
|
||||
Object.assign(state.trees[`${projectId}/${branchId}`], {
|
||||
tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
|
||||
tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)),
|
||||
});
|
||||
}
|
||||
},
|
||||
[types.UPDATE_TEMP_FLAG](state, { path, tempFile }) {
|
||||
Object.assign(state.entries[path], {
|
||||
tempFile,
|
||||
changed: tempFile,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_VIEWER](state, viewer) {
|
||||
Object.assign(state, {
|
||||
viewer,
|
||||
|
@ -100,6 +107,11 @@ export default {
|
|||
delayViewerUpdated,
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
|
||||
Object.assign(state, {
|
||||
fileFindVisible,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
|
||||
const changedFile = state.changedFiles.find(f => f.path === file.path);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ export default {
|
|||
[types.SET_FILE_ACTIVE](state, { path, active }) {
|
||||
Object.assign(state.entries[path], {
|
||||
active,
|
||||
lastOpenedAt: new Date().getTime(),
|
||||
});
|
||||
|
||||
if (active && !state.entries[path].pending) {
|
||||
|
|
|
@ -18,4 +18,5 @@ export default () => ({
|
|||
entries: {},
|
||||
viewer: 'editor',
|
||||
delayViewerUpdated: false,
|
||||
fileFindVisible: false,
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@ export const dataStructure = () => ({
|
|||
raw: '',
|
||||
content: '',
|
||||
parentTreeUrl: '',
|
||||
parentPath: '',
|
||||
renderError: false,
|
||||
base64: false,
|
||||
editorRow: 1,
|
||||
|
@ -42,6 +43,7 @@ export const dataStructure = () => ({
|
|||
viewMode: 'edit',
|
||||
previewMode: null,
|
||||
size: 0,
|
||||
lastOpenedAt: 0,
|
||||
});
|
||||
|
||||
export const decorateData = entity => {
|
||||
|
@ -64,6 +66,7 @@ export const decorateData = entity => {
|
|||
previewMode,
|
||||
file_lock,
|
||||
html,
|
||||
parentPath = '',
|
||||
} = entity;
|
||||
|
||||
return {
|
||||
|
@ -80,6 +83,7 @@ export const decorateData = entity => {
|
|||
opened,
|
||||
active,
|
||||
parentTreeUrl,
|
||||
parentPath,
|
||||
changed,
|
||||
renderError,
|
||||
content,
|
||||
|
@ -120,8 +124,8 @@ const sortTreesByTypeAndName = (a, b) => {
|
|||
} else if (a.type === 'blob' && b.type === 'tree') {
|
||||
return 1;
|
||||
}
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
||||
if (a.name < b.name) return -1;
|
||||
if (a.name > b.name) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ self.addEventListener('message', e => {
|
|||
|
||||
const treeList = [];
|
||||
let file;
|
||||
let parentPath;
|
||||
const entries = data.reduce((acc, path) => {
|
||||
const pathSplit = path.split('/');
|
||||
const blobName = pathSplit.pop().trim();
|
||||
|
@ -17,6 +18,8 @@ self.addEventListener('message', e => {
|
|||
const foundEntry = acc[folderPath];
|
||||
|
||||
if (!foundEntry) {
|
||||
parentPath = parentFolder ? parentFolder.path : null;
|
||||
|
||||
const tree = decorateData({
|
||||
projectId,
|
||||
branchId,
|
||||
|
@ -29,6 +32,7 @@ self.addEventListener('message', e => {
|
|||
tempFile,
|
||||
changed: tempFile,
|
||||
opened: tempFile,
|
||||
parentPath,
|
||||
});
|
||||
|
||||
Object.assign(acc, {
|
||||
|
@ -52,6 +56,8 @@ self.addEventListener('message', e => {
|
|||
|
||||
if (blobName !== '') {
|
||||
const fileFolder = acc[pathSplit.join('/')];
|
||||
parentPath = fileFolder ? fileFolder.path : null;
|
||||
|
||||
file = decorateData({
|
||||
projectId,
|
||||
branchId,
|
||||
|
@ -66,6 +72,7 @@ self.addEventListener('message', e => {
|
|||
content,
|
||||
base64,
|
||||
previewMode: viewerInformationForPath(blobName),
|
||||
parentPath,
|
||||
});
|
||||
|
||||
Object.assign(acc, {
|
||||
|
@ -86,5 +93,6 @@ self.addEventListener('message', e => {
|
|||
entries,
|
||||
treeList: sortTree(treeList),
|
||||
file,
|
||||
parentPath,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ export default {
|
|||
return timeIntervalInWords(this.job.queued);
|
||||
},
|
||||
runnerId() {
|
||||
return `#${this.job.runner.id}`;
|
||||
return `${this.job.runner.description} (#${this.job.runner.id})`;
|
||||
},
|
||||
retryButtonClass() {
|
||||
let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
|
||||
|
|
4
app/assets/javascripts/lib/utils/keycodes.js
Normal file
4
app/assets/javascripts/lib/utils/keycodes.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const UP_KEY_CODE = 38;
|
||||
export const DOWN_KEY_CODE = 40;
|
||||
export const ENTER_KEY_CODE = 13;
|
||||
export const ESC_KEY_CODE = 27;
|
|
@ -12,7 +12,8 @@ import ModalStore from './boards/stores/modal_store';
|
|||
export default class MilestoneSelect {
|
||||
constructor(currentProject, els, options = {}) {
|
||||
if (currentProject !== null) {
|
||||
this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
|
||||
this.currentProject =
|
||||
typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
|
||||
}
|
||||
|
||||
this.init(els, options);
|
||||
|
@ -26,7 +27,10 @@ export default class MilestoneSelect {
|
|||
}
|
||||
|
||||
$els.each((i, dropdown) => {
|
||||
let milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
|
||||
let milestoneLinkNoneTemplate,
|
||||
milestoneLinkTemplate,
|
||||
selectedMilestone,
|
||||
selectedMilestoneDefault;
|
||||
const $dropdown = $(dropdown);
|
||||
const projectId = $dropdown.data('projectId');
|
||||
const milestonesUrl = $dropdown.data('milestones');
|
||||
|
@ -46,45 +50,47 @@ export default class MilestoneSelect {
|
|||
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
|
||||
const $value = $block.find('.value');
|
||||
const $loading = $block.find('.block-loading').fadeOut();
|
||||
selectedMilestoneDefault = (showAny ? '' : null);
|
||||
selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
|
||||
selectedMilestoneDefault = showAny ? '' : null;
|
||||
selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault;
|
||||
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
|
||||
|
||||
if (issueUpdateURL) {
|
||||
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
|
||||
milestoneLinkTemplate = _.template(
|
||||
'<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
|
||||
);
|
||||
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
|
||||
}
|
||||
return $dropdown.glDropdown({
|
||||
showMenuAbove: showMenuAbove,
|
||||
data: (term, callback) => axios.get(milestonesUrl)
|
||||
.then(({ data }) => {
|
||||
data: (term, callback) =>
|
||||
axios.get(milestonesUrl).then(({ data }) => {
|
||||
const extraOptions = [];
|
||||
if (showAny) {
|
||||
extraOptions.push({
|
||||
id: 0,
|
||||
name: '',
|
||||
title: 'Any Milestone'
|
||||
id: null,
|
||||
name: null,
|
||||
title: 'Any Milestone',
|
||||
});
|
||||
}
|
||||
if (showNo) {
|
||||
extraOptions.push({
|
||||
id: -1,
|
||||
name: 'No Milestone',
|
||||
title: 'No Milestone'
|
||||
title: 'No Milestone',
|
||||
});
|
||||
}
|
||||
if (showUpcoming) {
|
||||
extraOptions.push({
|
||||
id: -2,
|
||||
name: '#upcoming',
|
||||
title: 'Upcoming'
|
||||
title: 'Upcoming',
|
||||
});
|
||||
}
|
||||
if (showStarted) {
|
||||
extraOptions.push({
|
||||
id: -3,
|
||||
name: '#started',
|
||||
title: 'Started'
|
||||
title: 'Started',
|
||||
});
|
||||
}
|
||||
if (extraOptions.length) {
|
||||
|
@ -106,7 +112,7 @@ export default class MilestoneSelect {
|
|||
`,
|
||||
filterable: true,
|
||||
search: {
|
||||
fields: ['title']
|
||||
fields: ['title'],
|
||||
},
|
||||
selectable: true,
|
||||
toggleLabel: (selected, el, e) => {
|
||||
|
@ -119,7 +125,7 @@ export default class MilestoneSelect {
|
|||
defaultLabel: defaultLabel,
|
||||
fieldName: $dropdown.data('fieldName'),
|
||||
text: milestone => _.escape(milestone.title),
|
||||
id: (milestone) => {
|
||||
id: milestone => {
|
||||
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
|
||||
return milestone.name;
|
||||
} else {
|
||||
|
@ -131,7 +137,7 @@ export default class MilestoneSelect {
|
|||
// display:block overrides the hide-collapse rule
|
||||
return $value.css('display', '');
|
||||
},
|
||||
opened: (e) => {
|
||||
opened: e => {
|
||||
const $el = $(e.currentTarget);
|
||||
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
|
||||
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
|
||||
|
@ -140,7 +146,7 @@ export default class MilestoneSelect {
|
|||
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
|
||||
},
|
||||
vue: $dropdown.hasClass('js-issue-board-sidebar'),
|
||||
clicked: (clickEvent) => {
|
||||
clicked: clickEvent => {
|
||||
const { $el, e } = clickEvent;
|
||||
let selected = clickEvent.selectedObj;
|
||||
|
||||
|
@ -155,11 +161,14 @@ export default class MilestoneSelect {
|
|||
|
||||
const page = $('body').attr('data-page');
|
||||
const isIssueIndex = page === 'projects:issues:index';
|
||||
const isMRIndex = (page === page && page === 'projects:merge_requests:index');
|
||||
const isSelecting = (selected.name !== selectedMilestone);
|
||||
const isMRIndex = page === page && page === 'projects:merge_requests:index';
|
||||
const isSelecting = selected.name !== selectedMilestone;
|
||||
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
|
||||
|
||||
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
|
||||
if (
|
||||
$dropdown.hasClass('js-filter-bulk-update') ||
|
||||
$dropdown.hasClass('js-issuable-form-dropdown')
|
||||
) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
@ -177,10 +186,13 @@ export default class MilestoneSelect {
|
|||
return $dropdown.closest('form').submit();
|
||||
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
|
||||
if (selected.id !== -1 && isSelecting) {
|
||||
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
|
||||
id: selected.id,
|
||||
title: selected.name
|
||||
}));
|
||||
gl.issueBoards.boardStoreIssueSet(
|
||||
'milestone',
|
||||
new ListMilestone({
|
||||
id: selected.id,
|
||||
title: selected.name,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
gl.issueBoards.boardStoreIssueDelete('milestone');
|
||||
}
|
||||
|
@ -188,7 +200,8 @@ export default class MilestoneSelect {
|
|||
$dropdown.trigger('loading.gl.dropdown');
|
||||
$loading.removeClass('hidden').fadeIn();
|
||||
|
||||
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
|
||||
gl.issueBoards.BoardsStore.detail.issue
|
||||
.update($dropdown.attr('data-issue-update'))
|
||||
.then(() => {
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
$loading.fadeOut();
|
||||
|
@ -203,7 +216,8 @@ export default class MilestoneSelect {
|
|||
data[abilityName].milestone_id = selected != null ? selected : null;
|
||||
$loading.removeClass('hidden').fadeIn();
|
||||
$dropdown.trigger('loading.gl.dropdown');
|
||||
return axios.put(issueUpdateURL, data)
|
||||
return axios
|
||||
.put(issueUpdateURL, data)
|
||||
.then(({ data }) => {
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
$loading.fadeOut();
|
||||
|
@ -215,7 +229,10 @@ export default class MilestoneSelect {
|
|||
data.milestone.name = data.milestone.title;
|
||||
$value.html(milestoneLinkTemplate(data.milestone));
|
||||
return $sidebarCollapsedValue
|
||||
.attr('data-original-title', `${data.milestone.name}<br />${data.milestone.remaining}`)
|
||||
.attr(
|
||||
'data-original-title',
|
||||
`${data.milestone.name}<br />${data.milestone.remaining}`,
|
||||
)
|
||||
.find('span')
|
||||
.text(data.milestone.title);
|
||||
} else {
|
||||
|
@ -230,7 +247,7 @@ export default class MilestoneSelect {
|
|||
$loading.fadeOut();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
|||
v-html="resolveSvg"
|
||||
></span>
|
||||
</span>
|
||||
<span class=".line-resolve-text">
|
||||
<span class="line-resolve-text">
|
||||
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => {
|
|||
scrollToElement(el);
|
||||
}
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => {
|
|||
|
||||
return Object.keys(resolvedMap).length;
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -52,16 +52,15 @@
|
|||
text() {
|
||||
const keepContributionsText = s__(`AdminArea|
|
||||
You are about to permanently delete the user %{username}.
|
||||
This will delete all of the issues, merge requests, and groups linked to them.
|
||||
Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
|
||||
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
|
||||
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
|
||||
|
||||
const deleteContributionsText = s__(`AdminArea|
|
||||
You are about to permanently delete the user %{username}.
|
||||
Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
|
||||
This will delete all of the issues, merge requests, and groups linked to them.
|
||||
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
|
||||
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
|
||||
|
||||
return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
|
||||
{
|
||||
username: `<strong>${_.escape(this.username)}</strong>`,
|
||||
|
|
|
@ -188,11 +188,11 @@ export default class ActivityCalendar {
|
|||
},
|
||||
{
|
||||
text: 'W',
|
||||
y: 29 + this.dayYPos(2),
|
||||
y: 29 + this.dayYPos(3),
|
||||
},
|
||||
{
|
||||
text: 'F',
|
||||
y: 29 + this.dayYPos(3),
|
||||
y: 29 + this.dayYPos(5),
|
||||
},
|
||||
];
|
||||
this.svg
|
||||
|
|
|
@ -5,7 +5,6 @@ import PerformanceBarService from '../services/performance_bar_service';
|
|||
import detailedMetric from './detailed_metric.vue';
|
||||
import requestSelector from './request_selector.vue';
|
||||
import simpleMetric from './simple_metric.vue';
|
||||
import upstreamPerformanceBar from './upstream_performance_bar.vue';
|
||||
|
||||
import Flash from '../../flash';
|
||||
|
||||
|
@ -14,7 +13,6 @@ export default {
|
|||
detailedMetric,
|
||||
requestSelector,
|
||||
simpleMetric,
|
||||
upstreamPerformanceBar,
|
||||
},
|
||||
props: {
|
||||
store: {
|
||||
|
@ -128,9 +126,6 @@ export default {
|
|||
{{ currentRequest.details.host.hostname }}
|
||||
</span>
|
||||
</div>
|
||||
<upstream-performance-bar
|
||||
v-if="initialRequest && currentRequest.details"
|
||||
/>
|
||||
<detailed-metric
|
||||
v-for="metric in $options.detailedMetrics"
|
||||
:key="metric.metric"
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
const upstreamPerformanceBar = document
|
||||
.getElementById('peek-view-performance-bar')
|
||||
.cloneNode(true);
|
||||
|
||||
upstreamPerformanceBar.classList.remove('hidden');
|
||||
|
||||
this.$refs.wrapper.appendChild(upstreamPerformanceBar);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
id="peek-view-performance-bar-vue"
|
||||
class="view"
|
||||
ref="wrapper"
|
||||
></div>
|
||||
</template>
|
|
@ -1,5 +1,3 @@
|
|||
import 'vendor/peek.performance_bar';
|
||||
|
||||
import Vue from 'vue';
|
||||
import performanceBarApp from './components/performance_bar_app.vue';
|
||||
import PerformanceBarStore from './stores/performance_bar_store';
|
||||
|
|
|
@ -32,26 +32,38 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
|
||||
buttonDisabled: {
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDisabled: false,
|
||||
linkRequested: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
cssClass() {
|
||||
const actionIconDash = dasherize(this.actionIcon);
|
||||
return `${actionIconDash} js-icon-${actionIconDash}`;
|
||||
},
|
||||
isDisabled() {
|
||||
return this.buttonDisabled === this.link;
|
||||
},
|
||||
watch: {
|
||||
requestFinishedFor() {
|
||||
if (this.requestFinishedFor === this.linkRequested) {
|
||||
this.isDisabled = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickAction() {
|
||||
$(this.$el).tooltip('hide');
|
||||
eventHub.$emit('graphAction', this.link);
|
||||
this.linkRequested = this.link;
|
||||
this.isDisabled = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -62,7 +74,8 @@ export default {
|
|||
@click="onClickAction"
|
||||
v-tooltip
|
||||
:title="tooltipText"
|
||||
class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
|
||||
class="js-ci-action btn btn-blank
|
||||
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
|
||||
:class="cssClass"
|
||||
data-container="body"
|
||||
:disabled="isDisabled"
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
<script>
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
|
||||
/**
|
||||
* Renders either a cancel, retry or play icon pointing to the given path.
|
||||
* TODO: Remove UJS from here and use an async request instead.
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
tooltipText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
actionMethod: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
actionIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
v-tooltip
|
||||
:data-method="actionMethod"
|
||||
:title="tooltipText"
|
||||
:href="link"
|
||||
rel="nofollow"
|
||||
class="ci-action-icon-wrapper js-ci-status-icon"
|
||||
data-container="body"
|
||||
aria-label="Job's action"
|
||||
>
|
||||
<icon :name="actionIcon" />
|
||||
</a>
|
||||
</template>
|
|
@ -1,77 +1,83 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import jobNameComponent from './job_name_component.vue';
|
||||
import jobComponent from './job_component.vue';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
import $ from 'jquery';
|
||||
import JobNameComponent from './job_name_component.vue';
|
||||
import JobComponent from './job_component.vue';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
|
||||
/**
|
||||
* Renders the dropdown for the pipeline graph.
|
||||
*
|
||||
* The following object should be provided as `job`:
|
||||
*
|
||||
* {
|
||||
* "id": 4256,
|
||||
* "name": "test",
|
||||
* "status": {
|
||||
* "icon": "icon_status_success",
|
||||
* "text": "passed",
|
||||
* "label": "passed",
|
||||
* "group": "success",
|
||||
* "details_path": "/root/ci-mock/builds/4256",
|
||||
* "action": {
|
||||
* "icon": "retry",
|
||||
* "title": "Retry",
|
||||
* "path": "/root/ci-mock/builds/4256/retry",
|
||||
* "method": "post"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
/**
|
||||
* Renders the dropdown for the pipeline graph.
|
||||
*
|
||||
* The following object should be provided as `job`:
|
||||
*
|
||||
* {
|
||||
* "id": 4256,
|
||||
* "name": "test",
|
||||
* "status": {
|
||||
* "icon": "icon_status_success",
|
||||
* "text": "passed",
|
||||
* "label": "passed",
|
||||
* "group": "success",
|
||||
* "details_path": "/root/ci-mock/builds/4256",
|
||||
* "action": {
|
||||
* "icon": "retry",
|
||||
* "title": "Retry",
|
||||
* "path": "/root/ci-mock/builds/4256/retry",
|
||||
* "method": "post"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
components: {
|
||||
JobComponent,
|
||||
JobNameComponent,
|
||||
},
|
||||
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
components: {
|
||||
jobComponent,
|
||||
jobNameComponent,
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
tooltipText() {
|
||||
return `${this.job.name} - ${this.job.status.label}`;
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
tooltipText() {
|
||||
return `${this.job.name} - ${this.job.status.label}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.stopDropdownClickPropagation();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.stopDropdownClickPropagation();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* When the user right clicks or cmd/ctrl + click in the job name
|
||||
* the dropdown should not be closed and the link should open in another tab,
|
||||
* so we stop propagation of the click event inside the dropdown.
|
||||
methods: {
|
||||
/**
|
||||
* When the user right clicks or cmd/ctrl + click in the job name or the action icon
|
||||
* the dropdown should not be closed so we stop propagation
|
||||
* of the click event inside the dropdown.
|
||||
*
|
||||
* Since this component is rendered multiple times per page we need to guarantee we only
|
||||
* target the click event of this component.
|
||||
*/
|
||||
stopDropdownClickPropagation() {
|
||||
$(this.$el
|
||||
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
|
||||
.on('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
stopDropdownClickPropagation() {
|
||||
$(
|
||||
'.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
|
||||
this.$el,
|
||||
).on('click', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="ci-job-dropdown-container">
|
||||
|
@ -101,8 +107,8 @@
|
|||
:key="i">
|
||||
<job-component
|
||||
:job="item"
|
||||
:is-dropdown="true"
|
||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -7,7 +7,6 @@ export default {
|
|||
StageColumnComponent,
|
||||
LoadingIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
|
@ -17,10 +16,10 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
actionDisabled: {
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -75,7 +74,7 @@ export default {
|
|||
:key="stage.name"
|
||||
:stage-connector-class="stageConnectorClass(index, stage)"
|
||||
:is-first-column="isFirstColumn(index)"
|
||||
:action-disabled="actionDisabled"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import ActionComponent from './action_component.vue';
|
||||
import DropdownActionComponent from './dropdown_action_component.vue';
|
||||
import JobNameComponent from './job_name_component.vue';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
|
||||
|
@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
|
|||
export default {
|
||||
components: {
|
||||
ActionComponent,
|
||||
DropdownActionComponent,
|
||||
JobNameComponent,
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
@ -44,26 +41,17 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
cssClassJobName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
isDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
actionDisabled: {
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
status() {
|
||||
return this.job && this.job.status ? this.job.status : {};
|
||||
|
@ -134,19 +122,11 @@ export default {
|
|||
</div>
|
||||
|
||||
<action-component
|
||||
v-if="hasAction && !isDropdown"
|
||||
v-if="hasAction"
|
||||
:tooltip-text="status.action.title"
|
||||
:link="status.action.path"
|
||||
:action-icon="status.action.icon"
|
||||
:button-disabled="actionDisabled"
|
||||
/>
|
||||
|
||||
<dropdown-action-component
|
||||
v-if="hasAction && isDropdown"
|
||||
:tooltip-text="status.action.title"
|
||||
:link="status.action.path"
|
||||
:action-icon="status.action.icon"
|
||||
:action-method="status.action.method"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -29,10 +29,11 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
actionDisabled: {
|
||||
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -74,12 +75,12 @@ export default {
|
|||
v-if="job.size === 1"
|
||||
:job="job"
|
||||
css-class-job-name="build-content"
|
||||
:action-disabled="actionDisabled"
|
||||
/>
|
||||
|
||||
<dropdown-job-component
|
||||
v-if="job.size > 1"
|
||||
:job="job"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
/>
|
||||
|
||||
</li>
|
||||
|
|
|
@ -25,7 +25,7 @@ export default () => {
|
|||
data() {
|
||||
return {
|
||||
mediator,
|
||||
actionDisabled: null,
|
||||
requestFinishedFor: null,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
@ -36,15 +36,17 @@ export default () => {
|
|||
},
|
||||
methods: {
|
||||
postAction(action) {
|
||||
this.actionDisabled = action;
|
||||
// Click was made, reset this variable
|
||||
this.requestFinishedFor = null;
|
||||
|
||||
this.mediator.service.postAction(action)
|
||||
this.mediator.service
|
||||
.postAction(action)
|
||||
.then(() => {
|
||||
this.mediator.refreshPipeline();
|
||||
this.actionDisabled = null;
|
||||
this.requestFinishedFor = action;
|
||||
})
|
||||
.catch(() => {
|
||||
this.actionDisabled = null;
|
||||
this.requestFinishedFor = action;
|
||||
Flash(__('An error occurred while making the request.'));
|
||||
});
|
||||
},
|
||||
|
@ -54,7 +56,7 @@ export default () => {
|
|||
props: {
|
||||
isLoading: this.mediator.state.isLoading,
|
||||
pipeline: this.mediator.store.state.pipeline,
|
||||
actionDisabled: this.actionDisabled,
|
||||
requestFinishedFor: this.requestFinishedFor,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -79,7 +81,8 @@ export default () => {
|
|||
},
|
||||
methods: {
|
||||
postAction(action) {
|
||||
this.mediator.service.postAction(action.path)
|
||||
this.mediator.service
|
||||
.postAction(action.path)
|
||||
.then(() => this.mediator.refreshPipeline())
|
||||
.catch(() => Flash(__('An error occurred while making the request.')));
|
||||
},
|
||||
|
|
|
@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr
|
|||
|
||||
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
|
||||
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
export const isLoading = state => state.isLoading;
|
||||
export const repos = state => state.repos;
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
export default {
|
||||
name: 'time-tracking-no-tracking-pane',
|
||||
template: `
|
||||
<div class="time-tracking-no-tracking-pane">
|
||||
<span class="no-value">
|
||||
{{ __('No estimate or time spent') }}
|
||||
</span>
|
||||
</div>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'TimeTrackingNoTrackingPane',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="time-tracking-no-tracking-pane">
|
||||
<span class="no-value">
|
||||
{{ __('No estimate or time spent') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
|
@ -1,3 +1,4 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
|
||||
|
@ -10,14 +11,17 @@ import Mediator from '../../sidebar_mediator';
|
|||
import eventHub from '../../event_hub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IssuableTimeTracker,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mediator: new Mediator(),
|
||||
store: new Store(),
|
||||
};
|
||||
},
|
||||
components: {
|
||||
IssuableTimeTracker,
|
||||
mounted() {
|
||||
this.listenForQuickActions();
|
||||
},
|
||||
methods: {
|
||||
listenForQuickActions() {
|
||||
|
@ -41,18 +45,17 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.listenForQuickActions();
|
||||
},
|
||||
template: `
|
||||
<div class="block">
|
||||
<issuable-time-tracker
|
||||
:time_estimate="store.timeEstimate"
|
||||
:time_spent="store.totalTimeSpent"
|
||||
:human_time_estimate="store.humanTimeEstimate"
|
||||
:human_time_spent="store.humanTotalTimeSpent"
|
||||
:rootPath="store.rootPath"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="block">
|
||||
<issuable-time-tracker
|
||||
:time_estimate="store.timeEstimate"
|
||||
:time_spent="store.totalTimeSpent"
|
||||
:human_time_estimate="store.humanTimeEstimate"
|
||||
:human_time_spent="store.humanTotalTimeSpent"
|
||||
:root-path="store.rootPath"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -2,7 +2,7 @@
|
|||
import TimeTrackingHelpState from './help_state.vue';
|
||||
import TimeTrackingCollapsedState from './collapsed_state.vue';
|
||||
import timeTrackingSpentOnlyPane from './spent_only_pane';
|
||||
import timeTrackingNoTrackingPane from './no_tracking_pane';
|
||||
import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
|
||||
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
|
||||
import TimeTrackingComparisonPane from './comparison_pane.vue';
|
||||
|
||||
|
@ -14,7 +14,7 @@ export default {
|
|||
TimeTrackingCollapsedState,
|
||||
TimeTrackingEstimateOnlyPane,
|
||||
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
|
||||
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
|
||||
TimeTrackingNoTrackingPane,
|
||||
TimeTrackingComparisonPane,
|
||||
TimeTrackingHelpState,
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
|
||||
function isValidProjectId(id) {
|
||||
return id > 0;
|
||||
|
@ -43,7 +44,7 @@ class SidebarMoveIssue {
|
|||
renderRow: project => `
|
||||
<li>
|
||||
<a href="#" class="js-move-issue-dropdown-item">
|
||||
${project.name_with_namespace}
|
||||
${_.escape(project.name_with_namespace)}
|
||||
</a>
|
||||
</li>
|
||||
`,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
|
||||
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
|
||||
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
|
||||
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
|
||||
import SidebarMoveIssue from './lib/sidebar_move_issue';
|
||||
|
|
|
@ -1,66 +1,70 @@
|
|||
<script>
|
||||
import { n__ } from '~/locale';
|
||||
import statusIcon from '../mr_widget_status_icon.vue';
|
||||
import eventHub from '../../event_hub';
|
||||
import { n__ } from '~/locale';
|
||||
import statusIcon from '../mr_widget_status_icon.vue';
|
||||
import eventHub from '../../event_hub';
|
||||
|
||||
export default {
|
||||
name: 'MRWidgetFailedToMerge',
|
||||
export default {
|
||||
name: 'MRWidgetFailedToMerge',
|
||||
|
||||
components: {
|
||||
statusIcon,
|
||||
components: {
|
||||
statusIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
mr: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
mr: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timer: 10,
|
||||
isRefreshing: false,
|
||||
intervalId: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
timerText() {
|
||||
return n__(
|
||||
'Refreshing in a second to show the updated status...',
|
||||
'Refreshing in %d seconds to show the updated status...',
|
||||
this.timer,
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
timer: 10,
|
||||
isRefreshing: false,
|
||||
};
|
||||
mounted() {
|
||||
this.intervalId = setInterval(this.updateTimer, 1000);
|
||||
},
|
||||
|
||||
created() {
|
||||
eventHub.$emit('DisablePolling');
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
refresh() {
|
||||
this.isRefreshing = true;
|
||||
eventHub.$emit('MRWidgetUpdateRequested');
|
||||
eventHub.$emit('EnablePolling');
|
||||
},
|
||||
updateTimer() {
|
||||
this.timer = this.timer - 1;
|
||||
|
||||
computed: {
|
||||
timerText() {
|
||||
return n__(
|
||||
'Refreshing in a second to show the updated status...',
|
||||
'Refreshing in %d seconds to show the updated status...',
|
||||
this.timer,
|
||||
);
|
||||
},
|
||||
if (this.timer === 0) {
|
||||
this.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
setInterval(() => {
|
||||
this.updateTimer();
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
created() {
|
||||
eventHub.$emit('DisablePolling');
|
||||
},
|
||||
|
||||
methods: {
|
||||
refresh() {
|
||||
this.isRefreshing = true;
|
||||
eventHub.$emit('MRWidgetUpdateRequested');
|
||||
eventHub.$emit('EnablePolling');
|
||||
},
|
||||
updateTimer() {
|
||||
this.timer = this.timer - 1;
|
||||
|
||||
if (this.timer === 0) {
|
||||
this.refresh();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="mr-widget-body media">
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import statusIcon from '../mr_widget_status_icon.vue';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
import eventHub from '../../event_hub';
|
||||
|
||||
export default {
|
||||
name: 'MRWidgetWIP',
|
||||
props: {
|
||||
mr: { type: Object, required: true },
|
||||
service: { type: Object, required: true },
|
||||
name: 'WorkInProgress',
|
||||
components: {
|
||||
statusIcon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
mr: { type: Object, required: true },
|
||||
service: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isMakingRequest: false,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
statusIcon,
|
||||
},
|
||||
methods: {
|
||||
removeWIP() {
|
||||
this.isMakingRequest = true;
|
||||
|
@ -36,32 +37,40 @@ export default {
|
|||
});
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="mr-widget-body media">
|
||||
<status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" />
|
||||
<div class="media-body space-children">
|
||||
<span class="bold">
|
||||
This is a Work in Progress
|
||||
<i
|
||||
v-tooltip
|
||||
class="fa fa-question-circle"
|
||||
title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
|
||||
aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
|
||||
</i>
|
||||
</span>
|
||||
<button
|
||||
v-if="mr.removeWIPPath"
|
||||
@click="removeWIP"
|
||||
:disabled="isMakingRequest"
|
||||
type="button"
|
||||
class="btn btn-default btn-xs js-remove-wip">
|
||||
<i
|
||||
v-if="isMakingRequest"
|
||||
class="fa fa-spinner fa-spin"
|
||||
aria-hidden="true" />
|
||||
Resolve WIP status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mr-widget-body media">
|
||||
<status-icon
|
||||
status="warning"
|
||||
:show-disabled-button="Boolean(mr.removeWIPPath)"
|
||||
/>
|
||||
<div class="media-body space-children">
|
||||
<span class="bold">
|
||||
This is a Work in Progress
|
||||
<i
|
||||
v-tooltip
|
||||
class="fa fa-question-circle"
|
||||
title="When this merge request is ready,
|
||||
remove the WIP: prefix from the title to allow it to be merged"
|
||||
aria-label="When this merge request is ready,
|
||||
remove the WIP: prefix from the title to allow it to be merged">
|
||||
</i>
|
||||
</span>
|
||||
<button
|
||||
v-if="mr.removeWIPPath"
|
||||
@click="removeWIP"
|
||||
:disabled="isMakingRequest"
|
||||
type="button"
|
||||
class="btn btn-default btn-xs js-remove-wip">
|
||||
<i
|
||||
v-if="isMakingRequest"
|
||||
class="fa fa-spinner fa-spin"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
Resolve WIP status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -21,7 +21,7 @@ export { default as MergedState } from './components/states/mr_widget_merged.vue
|
|||
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
|
||||
export { default as ClosedState } from './components/states/mr_widget_closed.vue';
|
||||
export { default as MergingState } from './components/states/mr_widget_merging.vue';
|
||||
export { default as WipState } from './components/states/mr_widget_wip';
|
||||
export { default as WorkInProgressState } from './components/states/work_in_progress.vue';
|
||||
export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
|
||||
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
|
||||
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
ClosedState,
|
||||
MergingState,
|
||||
RebaseState,
|
||||
WipState,
|
||||
WorkInProgressState,
|
||||
ArchivedState,
|
||||
ConflictsState,
|
||||
NothingToMergeState,
|
||||
|
@ -220,7 +220,7 @@ export default {
|
|||
'mr-widget-closed': ClosedState,
|
||||
'mr-widget-merging': MergingState,
|
||||
'mr-widget-failed-to-merge': FailedToMerge,
|
||||
'mr-widget-wip': WipState,
|
||||
'mr-widget-wip': WorkInProgressState,
|
||||
'mr-widget-archived': ArchivedState,
|
||||
'mr-widget-conflicts': ConflictsState,
|
||||
'mr-widget-nothing-to-merge': NothingToMergeState,
|
||||
|
|
|
@ -1,52 +1,52 @@
|
|||
<script>
|
||||
import ciIcon from './ci_icon.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
/**
|
||||
* Renders CI Badge link with CI icon and status text based on
|
||||
* API response shared between all places where it is used.
|
||||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
* text:"running" // text rendered
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Pipelines table - first column
|
||||
* - Jobs table - first column
|
||||
* - Pipeline show view - header
|
||||
* - Job show view - header
|
||||
* - MR widget
|
||||
*/
|
||||
import CiIcon from './ci_icon.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
/**
|
||||
* Renders CI Badge link with CI icon and status text based on
|
||||
* API response shared between all places where it is used.
|
||||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
* text:"running" // text rendered
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Pipelines table - first column
|
||||
* - Jobs table - first column
|
||||
* - Pipeline show view - header
|
||||
* - Job show view - header
|
||||
* - MR widget
|
||||
*/
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ciIcon,
|
||||
export default {
|
||||
components: {
|
||||
CiIcon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
showText: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showText: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cssClass() {
|
||||
const className = this.status.group;
|
||||
return className ? `ci-status ci-${className}` : 'ci-status';
|
||||
},
|
||||
computed: {
|
||||
cssClass() {
|
||||
const className = this.status.group;
|
||||
return className ? `ci-status ci-${className}` : 'ci-status';
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
|
|
|
@ -1,45 +1,44 @@
|
|||
<script>
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import Icon from '../../vue_shared/components/icon.vue';
|
||||
|
||||
/**
|
||||
* Renders CI icon based on API response shared between all places where it is used.
|
||||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
* text:"running" // text rendered
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Pipelines table Badge
|
||||
* - Pipelines table mini graph
|
||||
* - Pipeline graph
|
||||
* - Pipeline show view badge
|
||||
* - Jobs table
|
||||
* - Jobs show view header
|
||||
* - Jobs show view sidebar
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
/**
|
||||
* Renders CI icon based on API response shared between all places where it is used.
|
||||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
* text:"running" // text rendered
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Pipelines table Badge
|
||||
* - Pipelines table mini graph
|
||||
* - Pipeline graph
|
||||
* - Pipeline show view badge
|
||||
* - Jobs table
|
||||
* - Jobs show view header
|
||||
* - Jobs show view sidebar
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cssClass() {
|
||||
const status = this.status.group;
|
||||
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
|
||||
},
|
||||
|
||||
computed: {
|
||||
cssClass() {
|
||||
const status = this.status.group;
|
||||
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span :class="cssClass">
|
||||
|
|
|
@ -1,40 +1,50 @@
|
|||
<script>
|
||||
/**
|
||||
* Falls back to the code used in `copy_to_clipboard.js`
|
||||
*/
|
||||
import tooltip from '../directives/tooltip';
|
||||
/**
|
||||
* Falls back to the code used in `copy_to_clipboard.js`
|
||||
*
|
||||
* Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
|
||||
* when clicked.
|
||||
*
|
||||
* @example
|
||||
* <clipboard-button
|
||||
* title="Copy to clipbard"
|
||||
* text="Content to be copied"
|
||||
* css-class="btn-transparent"
|
||||
* />
|
||||
*/
|
||||
import tooltip from '../directives/tooltip';
|
||||
|
||||
export default {
|
||||
name: 'ClipboardButton',
|
||||
directives: {
|
||||
tooltip,
|
||||
export default {
|
||||
name: 'ClipboardButton',
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tooltipPlacement: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'top',
|
||||
},
|
||||
tooltipContainer: {
|
||||
type: [String, Boolean],
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
cssClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'btn-default',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
tooltipPlacement: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'top',
|
||||
},
|
||||
tooltipContainer: {
|
||||
type: [String, Boolean],
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
cssClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'btn-default',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,119 +1,111 @@
|
|||
<script>
|
||||
import commitIconSvg from 'icons/_icon_commit.svg';
|
||||
import userAvatarLink from './user_avatar/user_avatar_link.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
import Icon from '../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
UserAvatarLink,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* Indicates the existance of a tag.
|
||||
* Used to render the correct icon, if true will render `fa-tag` icon,
|
||||
* if false will render a svg sprite fork icon
|
||||
*/
|
||||
tag: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
components: {
|
||||
userAvatarLink,
|
||||
icon,
|
||||
/**
|
||||
* If provided is used to render the branch name and url.
|
||||
* Should contain the following properties:
|
||||
* name
|
||||
* ref_url
|
||||
*/
|
||||
commitRef: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
/**
|
||||
* Used to link to the commit sha.
|
||||
*/
|
||||
commitUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* Indicates the existance of a tag.
|
||||
* Used to render the correct icon, if true will render `fa-tag` icon,
|
||||
* if false will render a svg sprite fork icon
|
||||
*/
|
||||
tag: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* If provided is used to render the branch name and url.
|
||||
* Should contain the following properties:
|
||||
* name
|
||||
* ref_url
|
||||
*/
|
||||
commitRef: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
/**
|
||||
* Used to link to the commit sha.
|
||||
*/
|
||||
commitUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to show the commit short sha that links to the commit url.
|
||||
*/
|
||||
shortSha: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* If provided shows the commit tile.
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* If provided renders information about the author of the commit.
|
||||
* When provided should include:
|
||||
* `avatar_url` to render the avatar icon
|
||||
* `web_url` to link to user profile
|
||||
* `username` to render alt and title tags
|
||||
*/
|
||||
author: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
showBranch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* Used to show the commit short sha that links to the commit url.
|
||||
*/
|
||||
shortSha: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Used to verify if all the properties needed to render the commit
|
||||
* ref section were provided.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasCommitRef() {
|
||||
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
|
||||
},
|
||||
/**
|
||||
* Used to verify if all the properties needed to render the commit
|
||||
* author section were provided.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasAuthor() {
|
||||
return this.author &&
|
||||
this.author.avatar_url &&
|
||||
this.author.path &&
|
||||
this.author.username;
|
||||
},
|
||||
/**
|
||||
* If information about the author is provided will return a string
|
||||
* to be rendered as the alt attribute of the img tag.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
userImageAltDescription() {
|
||||
return this.author &&
|
||||
this.author.username ? `${this.author.username}'s avatar` : null;
|
||||
},
|
||||
/**
|
||||
* If provided shows the commit tile.
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
created() {
|
||||
this.commitIconSvg = commitIconSvg;
|
||||
/**
|
||||
* If provided renders information about the author of the commit.
|
||||
* When provided should include:
|
||||
* `avatar_url` to render the avatar icon
|
||||
* `web_url` to link to user profile
|
||||
* `username` to render alt and title tags
|
||||
*/
|
||||
author: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
};
|
||||
showBranch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Used to verify if all the properties needed to render the commit
|
||||
* ref section were provided.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasCommitRef() {
|
||||
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
|
||||
},
|
||||
/**
|
||||
* Used to verify if all the properties needed to render the commit
|
||||
* author section were provided.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasAuthor() {
|
||||
return this.author && this.author.avatar_url && this.author.path && this.author.username;
|
||||
},
|
||||
/**
|
||||
* If information about the author is provided will return a string
|
||||
* to be rendered as the alt attribute of the img tag.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
userImageAltDescription() {
|
||||
return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="branch-commit">
|
||||
|
@ -141,11 +133,10 @@
|
|||
{{ commitRef.name }}
|
||||
</a>
|
||||
</template>
|
||||
<div
|
||||
v-html="commitIconSvg"
|
||||
<icon
|
||||
name="commit"
|
||||
class="commit-icon js-commit-icon"
|
||||
>
|
||||
</div>
|
||||
/>
|
||||
|
||||
<a
|
||||
class="commit-sha"
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
/**
|
||||
* Port of detail_behavior expand button.
|
||||
*
|
||||
* @example
|
||||
* <expand-button>
|
||||
* <template slot="expanded">
|
||||
* Text goes here.
|
||||
* </template>
|
||||
* </expand-button>
|
||||
*/
|
||||
export default {
|
||||
name: 'ExpandButton',
|
||||
data() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
};
|
||||
import { __ } from '~/locale';
|
||||
/**
|
||||
* Port of detail_behavior expand button.
|
||||
*
|
||||
* @example
|
||||
* <expand-button>
|
||||
* <template slot="expanded">
|
||||
* Text goes here.
|
||||
* </template>
|
||||
* </expand-button>
|
||||
*/
|
||||
export default {
|
||||
name: 'ExpandButton',
|
||||
data() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ariaLabel() {
|
||||
return __('Click to expand text');
|
||||
},
|
||||
computed: {
|
||||
ariaLabel() {
|
||||
return __('Click to expand text');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import getIconForFile from './file_icon/file_icon_map';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import getIconForFile from './file_icon/file_icon_map';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
|
||||
/* This is a re-usable vue component for rendering a svg sprite
|
||||
/* This is a re-usable vue component for rendering a svg sprite
|
||||
icon
|
||||
|
||||
Sample configuration:
|
||||
|
@ -15,60 +15,60 @@
|
|||
/>
|
||||
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
loadingIcon,
|
||||
icon,
|
||||
export default {
|
||||
components: {
|
||||
loadingIcon,
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
fileName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
fileName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
folder: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
opened: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 16,
|
||||
},
|
||||
|
||||
cssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
folder: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
computed: {
|
||||
spriteHref() {
|
||||
const iconName = getIconForFile(this.fileName) || 'file';
|
||||
return `${gon.sprite_file_icons}#${iconName}`;
|
||||
},
|
||||
folderIconName() {
|
||||
return this.opened ? 'folder-open' : 'folder';
|
||||
},
|
||||
iconSizeClass() {
|
||||
return this.size ? `s${this.size}` : '';
|
||||
},
|
||||
|
||||
opened: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 16,
|
||||
},
|
||||
|
||||
cssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
spriteHref() {
|
||||
const iconName = getIconForFile(this.fileName) || 'file';
|
||||
return `${gon.sprite_file_icons}#${iconName}`;
|
||||
},
|
||||
folderIconName() {
|
||||
return this.opened ? 'folder-open' : 'folder';
|
||||
},
|
||||
iconSizeClass() {
|
||||
return this.size ? `s${this.size}` : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span>
|
||||
|
@ -82,6 +82,7 @@
|
|||
v-if="!loading && folder"
|
||||
:name="folderIconName"
|
||||
:size="size"
|
||||
css-classes="folder-icon"
|
||||
/>
|
||||
<loading-icon
|
||||
v-if="loading"
|
||||
|
|
|
@ -1,78 +1,78 @@
|
|||
<script>
|
||||
import ciIconBadge from './ci_badge_link.vue';
|
||||
import loadingIcon from './loading_icon.vue';
|
||||
import timeagoTooltip from './time_ago_tooltip.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
import userAvatarImage from './user_avatar/user_avatar_image.vue';
|
||||
import CiIconBadge from './ci_badge_link.vue';
|
||||
import LoadingIcon from './loading_icon.vue';
|
||||
import TimeagoTooltip from './time_ago_tooltip.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
|
||||
|
||||
/**
|
||||
* Renders header component for job and pipeline page based on UI mockups
|
||||
*
|
||||
* Used in:
|
||||
* - job show page
|
||||
* - pipeline show page
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
ciIconBadge,
|
||||
loadingIcon,
|
||||
timeagoTooltip,
|
||||
userAvatarImage,
|
||||
/**
|
||||
* Renders header component for job and pipeline page based on UI mockups
|
||||
*
|
||||
* Used in:
|
||||
* - job show page
|
||||
* - pipeline show page
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
CiIconBadge,
|
||||
LoadingIcon,
|
||||
TimeagoTooltip,
|
||||
UserAvatarImage,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
itemName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
itemName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
itemId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
time: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
actions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
hasSidebarButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
shouldRenderTriggeredLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
itemId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
time: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
actions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
hasSidebarButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
shouldRenderTriggeredLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
userAvatarAltText() {
|
||||
return `${this.user.name}'s avatar`;
|
||||
},
|
||||
computed: {
|
||||
userAvatarAltText() {
|
||||
return `${this.user.name}'s avatar`;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickAction(action) {
|
||||
this.$emit('actionClicked', action);
|
||||
},
|
||||
methods: {
|
||||
onClickAction(action) {
|
||||
this.$emit('actionClicked', action);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,76 +1,75 @@
|
|||
<script>
|
||||
/* This is a re-usable vue component for rendering a svg sprite
|
||||
icon
|
||||
|
||||
/* This is a re-usable vue component for rendering a svg sprite
|
||||
icon
|
||||
Sample configuration:
|
||||
|
||||
Sample configuration:
|
||||
<icon
|
||||
name="retry"
|
||||
:size="32"
|
||||
css-classes="top"
|
||||
/>
|
||||
|
||||
<icon
|
||||
name="retry"
|
||||
:size="32"
|
||||
css-classes="top"
|
||||
/>
|
||||
*/
|
||||
// only allow classes in images.scss e.g. s12
|
||||
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
|
||||
|
||||
*/
|
||||
// only allow classes in images.scss e.g. s12
|
||||
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 16,
|
||||
validator(value) {
|
||||
return validSizes.includes(value);
|
||||
},
|
||||
},
|
||||
|
||||
cssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
width: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
height: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
y: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
x: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 16,
|
||||
validator(value) {
|
||||
return validSizes.includes(value);
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
spriteHref() {
|
||||
return `${gon.sprite_icons}#${this.name}`;
|
||||
},
|
||||
iconSizeClass() {
|
||||
return this.size ? `s${this.size}` : '';
|
||||
},
|
||||
cssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
};
|
||||
|
||||
width: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
height: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
y: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
x: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
spriteHref() {
|
||||
return `${gon.sprite_icons}#${this.name}`;
|
||||
},
|
||||
iconSizeClass() {
|
||||
return this.size ? `s${this.size}` : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -79,7 +78,8 @@
|
|||
:width="width"
|
||||
:height="height"
|
||||
:x="x"
|
||||
:y="y">
|
||||
:y="y"
|
||||
>
|
||||
<use v-bind="{ 'xlink:href':spriteHref }" />
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -17,7 +17,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
/**
|
||||
* This method is based on app/helpers/application_helper.rb#project_identicon
|
||||
* This method is based on app/helpers/avatars_helper.rb#project_identicon
|
||||
*/
|
||||
identiconStyles() {
|
||||
const allowedColors = [
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { __ } from '~/locale';
|
||||
import LabelsSelect from '~/labels_select';
|
||||
import LoadingIcon from '../../loading_icon.vue';
|
||||
|
@ -98,11 +99,18 @@ export default {
|
|||
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
|
||||
handleClick: this.handleClick,
|
||||
});
|
||||
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
|
||||
},
|
||||
methods: {
|
||||
handleClick(label) {
|
||||
this.$emit('onLabelClick', label);
|
||||
},
|
||||
handleCollapsedValueClick() {
|
||||
this.$emit('toggleCollapse');
|
||||
},
|
||||
handleDropdownHidden() {
|
||||
this.$emit('onDropdownClose');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -112,6 +120,7 @@ export default {
|
|||
<dropdown-value-collapsed
|
||||
v-if="showCreate"
|
||||
:labels="context.labels"
|
||||
@onValueClick="handleCollapsedValueClick"
|
||||
/>
|
||||
<dropdown-title
|
||||
:can-edit="canEdit"
|
||||
|
@ -133,7 +142,10 @@ export default {
|
|||
:name="hiddenInputName"
|
||||
:label="label"
|
||||
/>
|
||||
<div class="dropdown">
|
||||
<div
|
||||
class="dropdown"
|
||||
ref="dropdown"
|
||||
>
|
||||
<dropdown-button
|
||||
:ability-name="abilityName"
|
||||
:field-name="hiddenInputName"
|
||||
|
|
|
@ -26,6 +26,11 @@ export default {
|
|||
return labelsString;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('onValueClick');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -36,6 +41,7 @@ export default {
|
|||
data-placement="left"
|
||||
data-container="body"
|
||||
:title="labelsList"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class ListLabel {
|
||||
export default class ListLabel {
|
||||
constructor(obj) {
|
||||
this.id = obj.id;
|
||||
this.title = obj.title;
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
}
|
||||
|
||||
&.middle-block {
|
||||
margin-top: 0;
|
||||
margin-top: $gl-padding-24;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@
|
|||
}
|
||||
|
||||
&.footer-block {
|
||||
margin-top: 0;
|
||||
margin-top: $gl-padding-24;
|
||||
border-bottom: 0;
|
||||
margin-bottom: -$gl-padding;
|
||||
}
|
||||
|
|
|
@ -452,6 +452,7 @@ img.emoji {
|
|||
|
||||
/** COMMON CLASSES **/
|
||||
.prepend-top-0 { margin-top: 0; }
|
||||
.prepend-top-2 { margin-top: 2px; }
|
||||
.prepend-top-5 { margin-top: 5px; }
|
||||
.prepend-top-8 { margin-top: $grid-size; }
|
||||
.prepend-top-10 { margin-top: 10px; }
|
||||
|
@ -472,6 +473,7 @@ img.emoji {
|
|||
.append-right-20 { margin-right: 20px; }
|
||||
.append-bottom-0 { margin-bottom: 0; }
|
||||
.append-bottom-5 { margin-bottom: 5px; }
|
||||
.append-bottom-8 { margin-bottom: $grid-size; }
|
||||
.append-bottom-10 { margin-bottom: 10px; }
|
||||
.append-bottom-15 { margin-bottom: 15px; }
|
||||
.append-bottom-20 { margin-bottom: 20px; }
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
border-color: $gray-darkest;
|
||||
}
|
||||
|
||||
[data-toggle="dropdown"] {
|
||||
[data-toggle='dropdown'] {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
@ -172,7 +172,11 @@
|
|||
color: $brand-danger;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.disable-hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:not(.disable-hover):hover,
|
||||
&:active,
|
||||
&:focus,
|
||||
&.is-focused {
|
||||
|
@ -508,17 +512,16 @@
|
|||
}
|
||||
|
||||
&.is-indeterminate::before {
|
||||
content: "\f068";
|
||||
content: '\f068';
|
||||
}
|
||||
|
||||
&.is-active::before {
|
||||
content: "\f00c";
|
||||
content: '\f00c';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dropdown-title {
|
||||
position: relative;
|
||||
padding: 2px 25px 10px;
|
||||
|
@ -724,7 +727,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.dropdown-menu-due-date {
|
||||
.dropdown-content {
|
||||
max-height: 230px;
|
||||
|
@ -854,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
|
|||
}
|
||||
|
||||
.projects-list-frequent-container,
|
||||
.projects-list-search-container, {
|
||||
.projects-list-search-container {
|
||||
padding: 8px 0;
|
||||
overflow-y: auto;
|
||||
|
||||
li.section-empty.section-failure {
|
||||
color: $callout-danger-color;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header,
|
||||
|
@ -867,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
|
|||
font-size: $gl-font-size;
|
||||
}
|
||||
|
||||
.projects-list-frequent-container,
|
||||
.projects-list-search-container {
|
||||
li.section-empty.section-failure {
|
||||
color: $callout-danger-color;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
position: relative;
|
||||
padding: 4px $gl-padding;
|
||||
|
@ -905,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
|
|||
}
|
||||
|
||||
.projects-list-item-container {
|
||||
.project-item-avatar-container
|
||||
.project-item-metadata-container {
|
||||
.project-item-avatar-container .project-item-metadata-container {
|
||||
float: left;
|
||||
}
|
||||
|
||||
|
|
|
@ -39,35 +39,10 @@
|
|||
svg {
|
||||
fill: currentColor;
|
||||
|
||||
&.s8 {
|
||||
@include svg-size(8px);
|
||||
}
|
||||
|
||||
&.s12 {
|
||||
@include svg-size(12px);
|
||||
}
|
||||
|
||||
&.s16 {
|
||||
@include svg-size(16px);
|
||||
}
|
||||
|
||||
&.s18 {
|
||||
@include svg-size(18px);
|
||||
}
|
||||
|
||||
&.s24 {
|
||||
@include svg-size(24px);
|
||||
}
|
||||
|
||||
&.s32 {
|
||||
@include svg-size(32px);
|
||||
}
|
||||
|
||||
&.s48 {
|
||||
@include svg-size(48px);
|
||||
}
|
||||
|
||||
&.s72 {
|
||||
@include svg-size(72px);
|
||||
$svg-sizes: 8 12 16 18 24 32 48 72;
|
||||
@each $svg-size in $svg-sizes {
|
||||
&.s#{$svg-size} {
|
||||
@include svg-size(#{$svg-size}px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,6 +107,16 @@
|
|||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.referenced-commands {
|
||||
background: $blue-50;
|
||||
padding: $gl-padding-8 $gl-padding;
|
||||
border-radius: $border-radius-default;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.md-preview-holder {
|
||||
min-height: 167px;
|
||||
padding: 10px 0;
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
.project-home-panel {
|
||||
padding-left: 0 !important;
|
||||
|
||||
.project-avatar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.project-repo-buttons,
|
||||
.git-clone-holder {
|
||||
display: none;
|
||||
|
|
|
@ -241,8 +241,6 @@
|
|||
}
|
||||
|
||||
.scrolling-tabs-container {
|
||||
position: relative;
|
||||
|
||||
.merge-request-tabs-container & {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -272,8 +270,6 @@
|
|||
}
|
||||
|
||||
.inner-page-scroll-tabs {
|
||||
position: relative;
|
||||
|
||||
.fade-right {
|
||||
@include fade(left, $white-light);
|
||||
right: 0;
|
||||
|
|
|
@ -212,6 +212,7 @@ $tooltip-font-size: 12px;
|
|||
/*
|
||||
* Padding
|
||||
*/
|
||||
$gl-padding-24: 24px;
|
||||
$gl-padding: 16px;
|
||||
$gl-padding-8: 8px;
|
||||
$gl-padding-4: 4px;
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
.fork-svg {
|
||||
margin-right: 4px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
}
|
||||
|
||||
.branch-info .commit-icon {
|
||||
margin-right: 3px;
|
||||
margin-right: 8px;
|
||||
|
||||
svg {
|
||||
top: 3px;
|
||||
|
|
|
@ -314,6 +314,10 @@
|
|||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
|
||||
&:hover .color-label {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.label {
|
||||
vertical-align: inherit;
|
||||
font-size: $label-font-size;
|
||||
|
|
|
@ -772,7 +772,3 @@ ul.notes {
|
|||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.line-resolve-text {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
|
@ -468,6 +468,14 @@
|
|||
margin-bottom: 10px;
|
||||
white-space: normal;
|
||||
|
||||
.ci-job-dropdown-container {
|
||||
// override dropdown.scss
|
||||
.dropdown-menu li button {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// ensure .build-content has hover style when action-icon is hovered
|
||||
.ci-job-dropdown-container:hover .build-content {
|
||||
@extend .build-content:hover;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
}
|
||||
|
||||
.ide-view {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: calc(100vh - #{$header-height});
|
||||
margin-top: 0;
|
||||
|
@ -54,6 +55,7 @@
|
|||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: inherit;
|
||||
line-height: 22px;
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
|
@ -66,6 +68,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ide-file-icon-holder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ide-file-changed-icon {
|
||||
margin-left: auto;
|
||||
|
||||
|
@ -76,7 +83,6 @@
|
|||
|
||||
.ide-new-btn {
|
||||
display: none;
|
||||
margin-bottom: -4px;
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
|
@ -89,10 +95,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.folder {
|
||||
svg {
|
||||
fill: $gl-text-color-secondary;
|
||||
}
|
||||
.folder-icon {
|
||||
fill: $gl-text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,6 +114,7 @@
|
|||
.file-col-commit-message {
|
||||
display: flex;
|
||||
overflow: visible;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
|
@ -437,7 +442,7 @@
|
|||
.projects-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
|
||||
.context-header {
|
||||
width: auto;
|
||||
|
@ -876,6 +881,26 @@
|
|||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
|
||||
.ide-file-finder-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.ide-file-finder {
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.highlighted {
|
||||
color: $blue-500;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-commit-message-field {
|
||||
height: 200px;
|
||||
background-color: $white-light;
|
||||
|
@ -946,3 +971,7 @@
|
|||
background: transparent;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.ide-new-modal-label {
|
||||
line-height: 34px;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
@import 'framework/variables';
|
||||
@import 'peek/views/performance_bar';
|
||||
@import 'peek/views/rblineprof';
|
||||
|
||||
#js-peek {
|
||||
|
|
|
@ -110,7 +110,8 @@ class ApplicationController < ActionController::Base
|
|||
def log_exception(exception)
|
||||
Raven.capture_exception(exception) if sentry_enabled?
|
||||
|
||||
application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace
|
||||
backtrace_cleaner = Gitlab.rails5? ? env["action_dispatch.backtrace_cleaner"] : env
|
||||
application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
|
||||
application_trace.map! { |t| " #{t}\n" }
|
||||
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
|
||||
end
|
||||
|
|
|
@ -23,6 +23,9 @@ module AuthenticatesWithTwoFactor
|
|||
#
|
||||
# Returns nil
|
||||
def prompt_for_two_factor(user)
|
||||
# Set @user for Devise views
|
||||
@user = user # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
return locked_user_redirect(user) unless user.can?(:log_in)
|
||||
|
||||
session[:otp_user_id] = user.id
|
||||
|
|
|
@ -57,7 +57,7 @@ module IssuableCollections
|
|||
out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
if out_of_range
|
||||
redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
|
||||
redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
|
||||
end
|
||||
|
||||
out_of_range
|
||||
|
@ -165,8 +165,8 @@ module IssuableCollections
|
|||
[:project, :author, :assignees, :labels, :milestone, project: :namespace]
|
||||
when 'MergeRequest'
|
||||
[
|
||||
:source_project, :target_project, :author, :assignee, :labels, :milestone,
|
||||
head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
|
||||
:target_project, :author, :assignee, :labels, :milestone,
|
||||
source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController
|
|||
def build_canonical_path(group)
|
||||
params[:group_id] = group.to_param
|
||||
|
||||
url_for(params)
|
||||
url_for(safe_params)
|
||||
end
|
||||
end
|
||||
|
|
31
app/controllers/ldap/omniauth_callbacks_controller.rb
Normal file
31
app/controllers/ldap/omniauth_callbacks_controller.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
def self.define_providers!
|
||||
return unless Gitlab::Auth::LDAP::Config.enabled?
|
||||
|
||||
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
|
||||
alias_method server['provider_name'], :ldap
|
||||
end
|
||||
end
|
||||
|
||||
# We only find ourselves here
|
||||
# if the authentication to LDAP was successful.
|
||||
def ldap
|
||||
sign_in_user_flow(Gitlab::Auth::LDAP::User)
|
||||
end
|
||||
|
||||
define_providers!
|
||||
|
||||
override :set_remember_me
|
||||
def set_remember_me(user)
|
||||
user.remember_me = params[:remember_me] if user.persisted?
|
||||
end
|
||||
|
||||
override :fail_login
|
||||
def fail_login(user)
|
||||
flash[:alert] = 'Access denied for your LDAP account.'
|
||||
|
||||
redirect_to new_user_session_path
|
||||
end
|
||||
end
|
|
@ -4,18 +4,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
|
||||
protect_from_forgery except: [:kerberos, :saml, :cas3]
|
||||
|
||||
Gitlab.config.omniauth.providers.each do |provider|
|
||||
define_method provider['name'] do
|
||||
handle_omniauth
|
||||
end
|
||||
def handle_omniauth
|
||||
omniauth_flow(Gitlab::Auth::OAuth)
|
||||
end
|
||||
|
||||
if Gitlab::Auth::LDAP::Config.enabled?
|
||||
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
|
||||
define_method server['provider_name'] do
|
||||
ldap
|
||||
end
|
||||
end
|
||||
AuthHelper.providers_for_base_controller.each do |provider|
|
||||
alias_method provider, :handle_omniauth
|
||||
end
|
||||
|
||||
# Extend the standard implementation to also increment
|
||||
|
@ -37,51 +31,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
error ||= exception.error if exception.respond_to?(:error)
|
||||
error ||= exception.message if exception.respond_to?(:message)
|
||||
error ||= env["omniauth.error.type"].to_s
|
||||
|
||||
error.to_s.humanize if error
|
||||
end
|
||||
|
||||
# We only find ourselves here
|
||||
# if the authentication to LDAP was successful.
|
||||
def ldap
|
||||
ldap_user = Gitlab::Auth::LDAP::User.new(oauth)
|
||||
ldap_user.save if ldap_user.changed? # will also save new users
|
||||
|
||||
@user = ldap_user.gl_user
|
||||
@user.remember_me = params[:remember_me] if ldap_user.persisted?
|
||||
|
||||
# Do additional LDAP checks for the user filter and EE features
|
||||
if ldap_user.allowed?
|
||||
if @user.two_factor_enabled?
|
||||
prompt_for_two_factor(@user)
|
||||
else
|
||||
log_audit_event(@user, with: oauth['provider'])
|
||||
sign_in_and_redirect(@user)
|
||||
end
|
||||
else
|
||||
fail_ldap_login
|
||||
end
|
||||
end
|
||||
|
||||
def saml
|
||||
if current_user
|
||||
log_audit_event(current_user, with: :saml)
|
||||
# Update SAML identity if data has changed.
|
||||
identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
|
||||
if identity.nil?
|
||||
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
|
||||
redirect_to profile_account_path, notice: 'Authentication method updated'
|
||||
else
|
||||
redirect_to after_sign_in_path_for(current_user)
|
||||
end
|
||||
else
|
||||
saml_user = Gitlab::Auth::Saml::User.new(oauth)
|
||||
saml_user.save if saml_user.changed?
|
||||
@user = saml_user.gl_user
|
||||
|
||||
continue_login_process
|
||||
end
|
||||
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
|
||||
handle_signup_error
|
||||
omniauth_flow(Gitlab::Auth::Saml)
|
||||
end
|
||||
|
||||
def omniauth_error
|
||||
|
@ -117,25 +72,36 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
|
||||
private
|
||||
|
||||
def handle_omniauth
|
||||
def omniauth_flow(auth_module, identity_linker: nil)
|
||||
if current_user
|
||||
# Add new authentication method
|
||||
current_user.identities
|
||||
.with_extern_uid(oauth['provider'], oauth['uid'])
|
||||
.first_or_create(extern_uid: oauth['uid'])
|
||||
log_audit_event(current_user, with: oauth['provider'])
|
||||
redirect_to profile_account_path, notice: 'Authentication method updated'
|
||||
else
|
||||
oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
|
||||
oauth_user.save
|
||||
@user = oauth_user.gl_user
|
||||
|
||||
continue_login_process
|
||||
identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth)
|
||||
|
||||
identity_linker.link
|
||||
|
||||
if identity_linker.changed?
|
||||
redirect_identity_linked
|
||||
elsif identity_linker.error_message.present?
|
||||
redirect_identity_link_failed(identity_linker.error_message)
|
||||
else
|
||||
redirect_identity_exists
|
||||
end
|
||||
else
|
||||
sign_in_user_flow(auth_module::User)
|
||||
end
|
||||
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
|
||||
handle_disabled_provider
|
||||
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
|
||||
handle_signup_error
|
||||
end
|
||||
|
||||
def redirect_identity_exists
|
||||
redirect_to after_sign_in_path_for(current_user)
|
||||
end
|
||||
|
||||
def redirect_identity_link_failed(error_message)
|
||||
redirect_to profile_account_path, notice: "Authentication failed: #{error_message}"
|
||||
end
|
||||
|
||||
def redirect_identity_linked
|
||||
redirect_to profile_account_path, notice: 'Authentication method updated'
|
||||
end
|
||||
|
||||
def handle_service_ticket(provider, ticket)
|
||||
|
@ -144,21 +110,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
session[:service_tickets][provider] = ticket
|
||||
end
|
||||
|
||||
def continue_login_process
|
||||
# Only allow properly saved users to login.
|
||||
if @user.persisted? && @user.valid?
|
||||
log_audit_event(@user, with: oauth['provider'])
|
||||
def sign_in_user_flow(auth_user_class)
|
||||
auth_user = auth_user_class.new(oauth)
|
||||
user = auth_user.find_and_update!
|
||||
|
||||
if @user.two_factor_enabled?
|
||||
params[:remember_me] = '1' if remember_me?
|
||||
prompt_for_two_factor(@user)
|
||||
if auth_user.valid_sign_in?
|
||||
log_audit_event(user, with: oauth['provider'])
|
||||
|
||||
set_remember_me(user)
|
||||
|
||||
if user.two_factor_enabled?
|
||||
prompt_for_two_factor(user)
|
||||
else
|
||||
remember_me(@user) if remember_me?
|
||||
sign_in_and_redirect(@user)
|
||||
sign_in_and_redirect(user)
|
||||
end
|
||||
else
|
||||
fail_login
|
||||
fail_login(user)
|
||||
end
|
||||
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
|
||||
handle_disabled_provider
|
||||
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
|
||||
handle_signup_error
|
||||
end
|
||||
|
||||
def handle_signup_error
|
||||
|
@ -178,18 +150,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
@oauth ||= request.env['omniauth.auth']
|
||||
end
|
||||
|
||||
def fail_login
|
||||
error_message = @user.errors.full_messages.to_sentence
|
||||
def fail_login(user)
|
||||
error_message = user.errors.full_messages.to_sentence
|
||||
|
||||
return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
|
||||
end
|
||||
|
||||
def fail_ldap_login
|
||||
flash[:alert] = 'Access denied for your LDAP account.'
|
||||
|
||||
redirect_to new_user_session_path
|
||||
end
|
||||
|
||||
def fail_auth0_login
|
||||
flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
|
||||
|
||||
|
@ -208,6 +174,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
.for_authentication.security_event
|
||||
end
|
||||
|
||||
def set_remember_me(user)
|
||||
return unless remember_me?
|
||||
|
||||
if user.two_factor_enabled?
|
||||
params[:remember_me] = '1'
|
||||
else
|
||||
remember_me(user)
|
||||
end
|
||||
end
|
||||
|
||||
def remember_me?
|
||||
request_params = request.env['omniauth.params']
|
||||
(request_params['remember_me'] == '1') if request_params.present?
|
||||
|
|
14
app/controllers/profiles/active_sessions_controller.rb
Normal file
14
app/controllers/profiles/active_sessions_controller.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class Profiles::ActiveSessionsController < Profiles::ApplicationController
|
||||
def index
|
||||
@sessions = ActiveSession.list(current_user)
|
||||
end
|
||||
|
||||
def destroy
|
||||
ActiveSession.destroy(current_user, params[:id])
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to profile_active_sessions_url, status: 302 }
|
||||
format.js { head :ok }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController
|
|||
params[:namespace_id] = project.namespace.to_param
|
||||
params[:project_id] = project.to_param
|
||||
|
||||
url_for(params)
|
||||
url_for(safe_params)
|
||||
end
|
||||
|
||||
def repository
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue