Merge branch 'master' into 'dm-document-role-maintainer'

# Conflicts:
#   doc/development/code_review.md
This commit is contained in:
Douwe Maan 2018-10-16 22:39:33 +00:00
commit a706b3735e
816 changed files with 13639 additions and 8415 deletions

View File

@ -1,22 +1,13 @@
env:
browser: true
es6: true
extends:
- airbnb-base
- prettier
- plugin:vue/recommended
- '@gitlab'
globals:
__webpack_public_path__: true
gl: false
gon: false
localStorage: false
parserOptions:
parser: babel-eslint
plugins:
- filenames
- import
- html
- promise
settings:
html/html-extensions:
- '.html'
@ -25,38 +16,12 @@ settings:
webpack:
config: './config/webpack.config.js'
rules:
filenames/match-regex:
- error
- '^[a-z0-9_]+$'
import/no-commonjs: error
promise/catch-or-return: error
no-param-reassign:
- error
- props: true
ignorePropertyModificationsFor:
- 'acc' # for reduce accumulators
- 'accumulator' # for reduce accumulators
- 'el' # for DOM elements
- 'element' # for DOM elements
- 'state' # for Vuex mutations
no-underscore-dangle:
- error
- allow:
- __
- _links
no-mixed-operators: off
vue/html-self-closing:
- error
- html:
void: always
normal: never
component: always
svg: always
math: always
camelcase:
- error
- properties: never
ignoreDestructuring: true
# Disabled for now, to make the airbnb-base 12.1.0 -> 13.1.0 update smoother
no-else-return:
- error

View File

@ -3,7 +3,9 @@ inherit_gem:
- rubocop-default.yml
inherit_from: .rubocop_todo.yml
require: ./rubocop/rubocop
require:
- ./rubocop/rubocop
- rubocop-rspec
AllCops:
TargetRailsVersion: 4.2
@ -48,12 +50,20 @@ Style/FrozenStringLiteralComment:
- 'danger/**/*'
- 'db/**/*'
- 'ee/**/*'
- 'lib/**/*'
- 'lib/gitlab/**/*'
- 'lib/tasks/**/*'
- 'qa/**/*'
- 'rubocop/**/*'
- 'scripts/**/*'
- 'spec/**/*'
RSpec/FilePath:
Exclude:
- 'qa/**/*'
- 'spec/javascripts/fixtures/*'
- 'ee/spec/javascripts/fixtures/*'
- 'spec/requests/api/v3/*'
Naming/FileName:
ExpectMatchingDefinition: true
Exclude:
@ -75,6 +85,7 @@ Naming/FileName:
- EE
- JSON
- LDAP
- SAML
- IO
- HMAC
- QA

View File

@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 11.3.5 (2018-10-15)
### Fixed (2 changes)
- Fix loading issue on some merge request discussion. !21982
- Fix project deletion when there is a export available. !22276
## 11.3.3 (2018-10-04)
- No changes.

View File

@ -64,228 +64,56 @@ As of July 2018, all the documentation for contributing to the GitLab project ha
## Contribute to GitLab
Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is easy for everyone.
For a first-time step-by-step guide to the contribution process, please see
["Contributing to GitLab"](https://about.gitlab.com/contributing/).
Looking for something to work on? Look for issues in the [Backlog (Accepting merge requests) milestone](#i-want-to-contribute).
GitLab comes in two flavors, GitLab Community Edition (CE) our free and open
source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for
abbreviation.
To get an overview of GitLab community membership including those that would be reviewing or merging your contributions, please visit [the community roles page](doc/development/contributing/community_roles.md).
If you want to know how the GitLab [core team]
operates please see [the GitLab contributing process](PROCESS.md).
[GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
This [documentation](doc/development/contributing/index.md#contribute-to-gitlab) has been moved.
## Security vulnerability disclosure
Please report suspected security vulnerabilities in private to
`support@gitlab.com`, also see the
[disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/).
Please do **NOT** create publicly viewable issues for suspected security
vulnerabilities.
This [documentation](doc/development/contributing/index.md#security-vulnerability-disclosure) has been moved.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
### Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at conduct@gitlab.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
This [documentation](doc/development/contributing/index.md#code-of-conduct) has been moved.
## Closing policy for issues and merge requests
GitLab is a popular open source project and the capacity to deal with issues
and merge requests is limited. Out of respect for our volunteers, issues and
merge requests not in line with the guidelines listed in this document may be
closed without notice.
Please treat our volunteers with courtesy and respect, it will go a long way
towards getting your issue resolved.
Issues and merge requests should be in English and contain appropriate language
for audiences of all ages.
If a contributor is no longer actively working on a submitted merge request
we can decide that the merge request will be finished by one of our
[Merge request coaches][team] or close the merge request. We make this decision
based on how important the change is for our product vision. If a Merge request
coach is going to finish the merge request we assign the
~"coach will finish" label.
This [documentation](doc/development/contributing/index.md#closing-policy-for-issues-and-merge-requests) has been moved.
## Helping others
Please help other GitLab users when you can.
The methods people will use to seek help can be found on the [getting help page][getting-help].
Sign up for the mailing list, answer GitLab questions on StackOverflow or
respond in the IRC channel. You can also sign up on [CodeTriage][codetriage] to help with
the remaining issues on the GitHub issue tracker.
This [documentation](doc/development/contributing/index.md#helping-others) has been moved.
## I want to contribute!
If you want to contribute to GitLab, [issues in the Backlog (Accepting merge requests)](https://gitlab.com/gitlab-org/gitlab-ce/issues?scope=all&utf8=✓&state=opened&assignee_id=0&milestone_title=Backlog%20(Accepting%20merge%20requests))
are a great place to start. Issues with a lower weight (1 or 2) are deemed
suitable for beginners. These issues will be of reasonable size and challenge,
for anyone to start contributing to GitLab. If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to
learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel
please consider we favor
[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution!
This [documentation](doc/development/contributing/index.md#i-want-to-contribute) has been moved.
## Contribution Flow
When contributing to GitLab, your merge request is subject to review by merge request maintainers of a particular specialty.
When you submit code to GitLab, we really want it to get merged, but there will be times when it will not be merged.
When maintainers are reading through a merge request they may request guidance from other maintainers. If merge request maintainers conclude that the code should not be merged, our reasons will be fully disclosed. If it has been decided that the code quality is not up to GitLabs standards, the merge request maintainer will refer the author to our docs and code style guides, and provide some guidance.
Sometimes style guides will be followed but the code will lack structural integrity, or the maintainer will have reservations about the codes overall quality. When there is a reservation the maintainer will inform the author and provide some guidance. The author may then choose to update the merge request. Once the merge request has been updated and reassigned to the maintainer, they will review the code again. Once the code has been resubmitted any number of times, the maintainer may choose to close the merge request with a summary of why it will not be merged, as well as some guidance. If the merge request is closed the maintainer will be open to discussion as to how to improve the code so it can be approved in the future.
GitLab will do its best to review community contributions as quickly as possible. Specially appointed developers review community contributions daily. You may take a look at the [team page](https://about.gitlab.com/team/) for the merge request coach who specializes in the type of code you have written and mention them in the merge request. For example, if you have written some JavaScript in your code then you should mention the frontend merge request coach. If your code has multiple disciplines you may mention multiple merge request coaches.
GitLab receives a lot of community contributions, so if your code has not been reviewed within 4 days of its initial submission feel free to re-mention the appropriate merge request coach.
When submitting code to GitLab, you may feel that your contribution requires the aid of an external library. If your code includes an external library please provide a link to the library, as well as reasons for including it.
When your code contains more than 500 changes, any major breaking changes, or an external library, `@mention` a maintainer in the merge request. If you are not sure who to mention, the reviewer will add one early in the merge request process.
[core team]: https://about.gitlab.com/core-team/
[team]: https://about.gitlab.com/team/
[getting-help]: https://about.gitlab.com/getting-help/
[codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[accepting-mrs-weight]: https://gitlab.com/gitlab-org/gitlab-ce/issues?assignee_id=0&label_name[]=Accepting%20Merge%20Requests&sort=weight_asc
[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues
[ee-tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues
[google-group]: https://groups.google.com/forum/#!forum/gitlabhq
[stackoverflow]: https://stackoverflow.com/questions/tagged/gitlab
[fpl]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=feature+proposal
[accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests
[accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests
[gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
[definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html
[contributor-covenant]: http://contributor-covenant.org
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[changelog]: doc/development/changelog.md "Generate a changelog entry"
[doc-guidelines]: doc/development/documentation/index.md "Documentation guidelines"
[js-styleguide]: doc/development/fe_guide/style_guide_js.md "JavaScript styleguide"
[scss-styleguide]: doc/development/fe_guide/style_guide_scss.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[testing]: doc/development/testing_guide/index.md
[us-english]: https://en.wikipedia.org/wiki/American_English
This [documentation](doc/development/contributing/index.md) has been moved.
## Workflow labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Type labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Subject labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Team labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Release Scoping labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Priority labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Severity labels
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
@ -294,17 +122,14 @@ This [documentation](doc/development/contributing/issue_workflow.md) has been mo
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Label for community contributors
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
## Implement design & UI elements
This [documentation](doc/development/contributing/design.md) has been moved.
## Issue tracker
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
@ -313,7 +138,6 @@ This [documentation](doc/development/contributing/issue_workflow.md) has been mo
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Feature proposals
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
@ -322,32 +146,26 @@ This [documentation](doc/development/contributing/issue_workflow.md) has been mo
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Issue weight
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Regression issues
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Technical and UX debt
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
### Stewardship
This [documentation](doc/development/contributing/issue_workflow.md) has been moved.
## Merge requests
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
### Merge request guidelines
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
@ -357,12 +175,10 @@ This [documentation](doc/development/contributing/merge_request_workflow.md) has
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
## Definition of done
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
## Style guides
This [documentation](doc/development/contributing/design.md) has been moved.

View File

@ -1 +1 @@
0.125.0
0.125.1

View File

@ -1 +1 @@
1.2.0
1.2.1

View File

@ -1 +1 @@
8.3.3
8.4.0

View File

@ -83,9 +83,6 @@ gem 'net-ldap'
# Only used to compute wiki page slugs
gem 'gitlab-gollum-lib', '~> 4.2', require: false
# Language detection
gem 'github-linguist', '~> 5.3.3', require: 'linguist'
# API
gem 'grape', '~> 1.1'
gem 'grape-entity', '~> 0.7.1'
@ -146,6 +143,7 @@ gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
gem 'nokogiri', '~> 1.8.2'
gem 'escape_utils', '~> 1.1'
# Calendar rendering
gem 'icalendar'
@ -421,7 +419,7 @@ end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
gem 'grpc', '~> 1.15.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'

View File

@ -277,11 +277,6 @@ GEM
gitaly-proto (0.118.1)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.25.1)
github-markup (1.7.0)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
@ -328,15 +323,14 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1)
googleapis-common-protos-types (1.0.2)
google-protobuf (~> 3.0)
googleauth (0.6.2)
googleauth (0.6.6)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
logging (~> 2.0)
memoist (~> 0.12)
multi_json (~> 1.11)
os (~> 0.9)
os (>= 0.9, < 2.0)
signet (~> 0.7)
gpgme (2.0.13)
mini_portile2 (~> 2.1)
@ -360,10 +354,9 @@ GEM
railties
sprockets-rails
graphql (1.8.1)
grpc (1.11.0)
grpc (1.15.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
haml (5.0.4)
temple (>= 0.8.0)
tilt
@ -465,11 +458,7 @@ GEM
xml-simple
licensee (8.9.2)
rugged (~> 0.24)
little-plugger (1.1.4)
locale (2.1.2)
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
lograge (0.10.0)
actionpack (>= 4)
activesupport (>= 4)
@ -575,7 +564,7 @@ GEM
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
os (0.9.6)
os (1.0.0)
parallel (1.12.1)
parser (2.5.1.0)
ast (~> 2.4.0)
@ -843,7 +832,7 @@ GEM
sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1)
signet (0.8.1)
signet (0.11.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
@ -1006,6 +995,7 @@ DEPENDENCIES
ed25519 (~> 1.2)
email_reply_trimmer (~> 0.1)
email_spec (~> 2.2.0)
escape_utils (~> 1.1)
factory_bot_rails (~> 4.8.2)
faraday (~> 0.12)
fast_blank
@ -1028,7 +1018,6 @@ DEPENDENCIES
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.118.1)
github-linguist (~> 5.3.3)
github-markup (~> 1.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@ -1046,7 +1035,7 @@ DEPENDENCIES
grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0)
grpc (~> 1.11.0)
grpc (~> 1.15.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
@ -1187,4 +1176,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.16.4
1.16.6

View File

@ -280,11 +280,6 @@ GEM
gitaly-proto (0.118.1)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.25.1)
github-markup (1.7.0)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
@ -331,15 +326,14 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1)
googleapis-common-protos-types (1.0.2)
google-protobuf (~> 3.0)
googleauth (0.6.2)
googleauth (0.6.6)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
logging (~> 2.0)
memoist (~> 0.12)
multi_json (~> 1.11)
os (~> 0.9)
os (>= 0.9, < 2.0)
signet (~> 0.7)
gpgme (2.0.13)
mini_portile2 (~> 2.1)
@ -363,10 +357,9 @@ GEM
railties
sprockets-rails
graphql (1.8.1)
grpc (1.11.0)
grpc (1.15.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
haml (5.0.4)
temple (>= 0.8.0)
tilt
@ -468,11 +461,7 @@ GEM
xml-simple
licensee (8.9.2)
rugged (~> 0.24)
little-plugger (1.1.4)
locale (2.1.2)
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
lograge (0.10.0)
actionpack (>= 4)
activesupport (>= 4)
@ -579,7 +568,7 @@ GEM
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
os (0.9.6)
os (1.0.0)
parallel (1.12.1)
parser (2.5.1.0)
ast (~> 2.4.0)
@ -851,7 +840,7 @@ GEM
sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1)
signet (0.8.1)
signet (0.11.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
@ -1015,6 +1004,7 @@ DEPENDENCIES
ed25519 (~> 1.2)
email_reply_trimmer (~> 0.1)
email_spec (~> 2.2.0)
escape_utils (~> 1.1)
factory_bot_rails (~> 4.8.2)
faraday (~> 0.12)
fast_blank
@ -1037,7 +1027,6 @@ DEPENDENCIES
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.118.1)
github-linguist (~> 5.3.3)
github-markup (~> 1.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@ -1055,7 +1044,7 @@ DEPENDENCIES
grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0)
grpc (~> 1.11.0)
grpc (~> 1.15.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
@ -1196,4 +1185,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.16.4
1.16.6

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 312 B

View File

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

View File

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

View File

@ -4,20 +4,17 @@ import { n__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue';
import BoardBlankState from './board_blank_state.vue';
import './board_delete';
import BoardDelete from './board_delete';
import BoardList from './board_list.vue';
import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({
export default Vue.extend({
components: {
boardList,
'board-delete': gl.issueBoards.BoardDelete,
BoardBlankState,
BoardDelete,
BoardList,
Icon,
},
directives: {
@ -47,8 +44,8 @@ gl.issueBoards.Board = Vue.extend({
},
data () {
return {
detailIssue: Store.detail,
filter: Store.filter,
detailIssue: boardsStore.detail,
filter: boardsStore.filter,
};
},
computed: {
@ -70,20 +67,20 @@ gl.issueBoards.Board = Vue.extend({
}
},
mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
this.sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
handle: '.js-board-handle',
onEnd: (e) => {
gl.issueBoards.onEnd();
sortableEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray();
const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10));
this.$nextTick(() => {
Store.moveList(list, order);
boardsStore.moveList(list, order);
});
}
}

View File

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

View File

@ -2,8 +2,7 @@
/* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue';
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
import boardsStore from '../stores/boards_store';
export default {
name: 'BoardsIssueCard',
@ -42,7 +41,7 @@
data() {
return {
showDetail: false,
detailIssue: Store.detail,
detailIssue: boardsStore.detail,
};
},
computed: {
@ -63,11 +62,11 @@
if (this.showDetail) {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
eventHub.$emit('clearDetailIssue');
} else {
eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list;
boardsStore.detail.list = this.list;
}
}
},

View File

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

View File

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

View File

@ -4,8 +4,7 @@ import { Button } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
const Store = gl.issueBoards.BoardsStore;
import boardsStore from '../stores/boards_store';
export default {
name: 'BoardNewIssue',
@ -68,8 +67,8 @@ export default {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
Store.detail.issue = issue;
Store.detail.list = this.list;
boardsStore.detail.issue = issue;
boardsStore.detail.list = this.list;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions

View File

@ -14,13 +14,9 @@ import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import MilestoneSelect from '../../milestone_select';
import boardsStore from '../stores/boards_store';
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({
export default Vue.extend({
components: {
AssigneeTitle,
Assignees,
@ -35,7 +31,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
},
data() {
return {
detail: Store.detail,
detail: boardsStore.detail,
issue: {},
list: {},
loadingAssignees: false,
@ -117,18 +113,18 @@ gl.issueBoards.BoardSidebar = Vue.extend({
this.saveAssignees();
},
removeAssignee (a) {
gl.issueBoards.BoardsStore.detail.issue.removeAssignee(a);
boardsStore.detail.issue.removeAssignee(a);
},
addAssignee (a) {
gl.issueBoards.BoardsStore.detail.issue.addAssignee(a);
boardsStore.detail.issue.addAssignee(a);
},
removeAllAssignees () {
gl.issueBoards.BoardsStore.detail.issue.removeAllAssignees();
boardsStore.detail.issue.removeAllAssignees();
},
saveAssignees () {
this.loadingAssignees = true;
gl.issueBoards.BoardsStore.detail.issue.update()
boardsStore.detail.issue.update()
.then(() => {
this.loadingAssignees = false;
})

View File

@ -1,14 +1,15 @@
<script>
import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
import tooltip from '../../vue_shared/directives/tooltip';
const Store = gl.issueBoards.BoardsStore;
import boardsStore from '../stores/boards_store';
export default {
components: {
UserAvatarLink,
Icon,
},
directives: {
tooltip,
@ -110,7 +111,7 @@
filterByLabel(label, e) {
if (!this.updateFilters) return;
const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
const filterPath = boardsStore.filter.path.split('&');
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param);
@ -122,9 +123,9 @@
filterPath.splice(labelIndex, 1);
}
gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
boardsStore.filter.path = filterPath.join('&');
Store.updateFiltersUrl();
boardsStore.updateFiltersUrl();
eventHub.$emit('updateTokens');
},
@ -141,11 +142,11 @@
<div>
<div class="board-card-header">
<h4 class="board-card-title">
<i
<icon
v-if="issue.confidential"
class="fa fa-eye-slash confidential-icon"
aria-hidden="true"
></i>
name="eye-slash"
class="confidential-icon"
/>
<a
:href="issue.path"
:title="issue.title"

View File

@ -5,6 +5,7 @@ import ListsDropdown from './lists_dropdown.vue';
import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
import boardsStore from '../../stores/boards_store';
export default {
components: {
@ -14,7 +15,7 @@ export default {
data() {
return {
modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state,
state: boardsStore.state,
};
},
computed: {

View File

@ -1,4 +1,5 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import bp from '../../../breakpoints';
import ModalStore from '../../stores/modal_store';
import IssueCardInner from '../issue_card_inner.vue';
@ -6,6 +7,7 @@
export default {
components: {
IssueCardInner,
Icon,
},
props: {
issueLinkBase: {
@ -147,13 +149,13 @@
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"/>
<span
<icon
v-if="issue.selected"
:aria-label="'Issue #' + issue.id + ' selected'"
name="mobile-issue-close"
aria-checked="true"
class="issue-card-selected text-center">
<i class="fa fa-check"></i>
</span>
class="issue-card-selected text-center"
/>
</div>
</div>
</div>

View File

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

View File

@ -4,16 +4,12 @@ import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import _ from 'underscore';
import CreateLabelDropdown from '../../create_label';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
const Store = gl.issueBoards.BoardsStore;
import boardsStore from '../stores/boards_store';
$(document).off('created.label').on('created.label', (e, label) => {
Store.new({
boardsStore.new({
title: label.title,
position: Store.state.lists.length - 2,
position: boardsStore.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
@ -23,7 +19,7 @@ $(document).off('created.label').on('created.label', (e, label) => {
});
});
gl.issueBoards.newListDropdownInit = () => {
export default function initNewListDropdown() {
$('.js-new-board-list').each(function () {
const $this = $(this);
new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath'));
@ -36,7 +32,7 @@ gl.issueBoards.newListDropdownInit = () => {
});
},
renderRow (label) {
const active = Store.findList('title', label.title);
const active = boardsStore.findList('title', label.title);
const $li = $('<li />');
const $a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''),
@ -62,10 +58,10 @@ gl.issueBoards.newListDropdownInit = () => {
const label = options.selectedObj;
e.preventDefault();
if (!Store.findList('title', label.title)) {
Store.new({
if (!boardsStore.findList('title', label.title)) {
boardsStore.new({
title: label.title,
position: Store.state.lists.length - 2,
position: boardsStore.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
@ -74,9 +70,9 @@ gl.issueBoards.newListDropdownInit = () => {
},
});
Store.state.lists = _.sortBy(Store.state.lists, 'position');
boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position');
}
},
});
});
};
}

View File

@ -1,11 +1,15 @@
<script>
import $ from 'jquery';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../eventhub';
import Api from '../../api';
export default {
name: 'BoardProjectSelect',
components: {
Icon,
},
props: {
groupId: {
type: Number,
@ -78,11 +82,9 @@ export default {
aria-expanded="false"
>
{{ selectedProjectName }}
<i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
<icon
name="chevron-down"
/>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">
@ -92,12 +94,11 @@ export default {
type="button"
class="dropdown-title-button dropdown-menu-close"
>
<i
aria-hidden="true"
<icon
name="merge-request-close-m"
data-hidden="true"
class="fa fa-times dropdown-menu-close-icon"
>
</i>
class="dropdown-menu-close-icon"
/>
</button>
</div>
<div class="dropdown-input">
@ -106,12 +107,11 @@ export default {
type="search"
placeholder="Search projects"
/>
<i
aria-hidden="true"
<icon
name="search"
class="dropdown-input-search"
data-hidden="true"
class="fa fa-search dropdown-input-search"
>
</i>
/>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">

View File

@ -2,8 +2,7 @@
import Vue from 'vue';
import Flash from '../../../flash';
import { __ } from '../../../locale';
const Store = gl.issueBoards.BoardsStore;
import boardsStore from '../../stores/boards_store';
export default Vue.extend({
props: {
@ -49,7 +48,7 @@
list.removeIssue(issue);
});
Store.detail.issue = {};
boardsStore.detail.issue = {};
},
/**
* Build the default patch request.

View File

@ -1,5 +1,6 @@
import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager';
import boardsStore from './stores/boards_store';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
@ -23,7 +24,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
this.store.path = path.substr(1);
if (this.updateUrl) {
gl.issueBoards.BoardsStore.updateFiltersUrl();
boardsStore.updateFiltersUrl();
}
}

View File

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

View File

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

View File

@ -6,6 +6,7 @@
import Vue from 'vue';
import '~/vue_shared/models/label';
import IssueProject from './project';
import boardsStore from '../stores/boards_store';
class ListIssue {
constructor (obj, defaultAvatar) {
@ -86,7 +87,7 @@ class ListIssue {
}
getLists () {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
return boardsStore.state.lists.filter(list => list.findIssue(this.id));
}
updateData(newData) {

View File

@ -5,6 +5,7 @@ import { __ } from '~/locale';
import ListLabel from '~/vue_shared/models/label';
import ListAssignee from '~/vue_shared/models/assignee';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import boardsStore from '../stores/boards_store';
const PER_PAGE = 20;
@ -89,9 +90,9 @@ class List {
}
destroy() {
const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
const index = boardsStore.state.lists.indexOf(this);
boardsStore.state.lists.splice(index, 1);
boardsStore.updateNewListDropdown(this.id);
gl.boardService.destroyList(this.id).catch(() => {
// TODO: handle request error
@ -116,7 +117,7 @@ class List {
getIssues(emptyIssues = true) {
const data = {
...urlParamsToObject(gl.issueBoards.BoardsStore.filter.path),
...urlParamsToObject(boardsStore.filter.path),
page: this.page,
};

View File

@ -3,13 +3,11 @@
import $ from 'jquery';
import _ from 'underscore';
import Vue from 'vue';
import Cookies from 'js-cookie';
import { getUrlParamsArray } from '~/lib/utils/common_utils';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardsStore = {
const boardsStore = {
disabled: false,
filter: {
path: '',
@ -167,3 +165,16 @@ gl.issueBoards.BoardsStore = {
window.history.pushState(null, null, `?${this.filter.path}`);
}
};
// hacks added in order to allow milestone_select to function properly
// TODO: remove these
export function boardStoreIssueSet(...args) {
Vue.set(boardsStore.detail.issue, ...args);
}
export function boardStoreIssueDelete(...args) {
Vue.delete(boardsStore.detail.issue, ...args);
}
export default boardsStore;

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script>
import _ from 'underscore';
import helmInstallIllustration from '@gitlab-org/gitlab-svgs/illustrations/kubernetes-installation.svg';
import helmInstallIllustration from '@gitlab-org/gitlab-svgs/dist/illustrations/kubernetes-installation.svg';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png';

View File

@ -1,7 +1,11 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
export default {
components: {
Icon,
},
props: {
documentationLink: {
type: String,
@ -28,10 +32,9 @@
type="button"
@click="dismissOverviewDialog"
>
<i
class="fa fa-times"
aria-hidden="true">
</i>
<icon
name="close"
/>
</button>
<div
class="svg-container"

View File

@ -115,7 +115,7 @@ export default {
<span>
{{ selectedVersionName }}
</span>
<Icon
<icon
:size="12"
name="angle-down"
class="position-absolute"

View File

@ -20,6 +20,11 @@ export default {
Tooltip,
},
props: {
discussionPath: {
type: String,
required: false,
default: '',
},
diffFile: {
type: Object,
required: true,
@ -65,8 +70,7 @@ export default {
if (this.diffFile.submodule) {
return this.diffFile.submoduleTreeUrl || this.diffFile.submoduleLink;
}
return `#${this.diffFile.fileHash}`;
return this.discussionPath;
},
filePath() {
if (this.diffFile.submodule) {
@ -152,7 +156,7 @@ export default {
v-once
ref="titleWrapper"
:href="titleLink"
class="append-right-4"
class="append-right-4 js-title-wrapper"
>
<file-icon
:file-name="filePath"

View File

@ -1,5 +1,6 @@
import Cookies from 'js-cookie';
import { getParameterValues } from '~/lib/utils/url_utility';
import bp from '~/breakpoints';
import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants';
const viewTypeFromQueryString = getParameterValues('view')[0];
@ -20,6 +21,7 @@ export default () => ({
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
tree: [],
treeEntries: {},
showTreeList: storedTreeShow === null ? true : storedTreeShow === 'true',
showTreeList:
storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : storedTreeShow === 'true',
currentDiffFileId: '',
});

View File

@ -0,0 +1,13 @@
import DirtySubmitForm from './dirty_submit_form';
class DirtySubmitCollection {
constructor(forms) {
this.forms = forms;
this.dirtySubmits = [];
this.forms.forEach(form => this.dirtySubmits.push(new DirtySubmitForm(form)));
}
}
export default DirtySubmitCollection;

View File

@ -0,0 +1,9 @@
import DirtySubmitCollection from './dirty_submit_collection';
import DirtySubmitForm from './dirty_submit_form';
export default function dirtySubmitFactory(formOrForms) {
const isCollection = formOrForms instanceof NodeList || formOrForms instanceof Array;
const DirtySubmitClass = isCollection ? DirtySubmitCollection : DirtySubmitForm;
return new DirtySubmitClass(formOrForms);
}

View File

@ -0,0 +1,82 @@
import _ from 'underscore';
class DirtySubmitForm {
constructor(form) {
this.form = form;
this.dirtyInputs = [];
this.isDisabled = true;
this.init();
}
init() {
this.inputs = this.form.querySelectorAll('input, textarea, select');
this.submits = this.form.querySelectorAll('input[type=submit], button[type=submit]');
this.inputs.forEach(DirtySubmitForm.initInput);
this.toggleSubmission();
this.registerListeners();
}
registerListeners() {
const throttledUpdateDirtyInput = _.throttle(
event => this.updateDirtyInput(event),
DirtySubmitForm.THROTTLE_DURATION,
);
this.form.addEventListener('input', throttledUpdateDirtyInput);
this.form.addEventListener('submit', event => this.formSubmit(event));
}
updateDirtyInput(event) {
const input = event.target;
if (!input.dataset.dirtySubmitOriginalValue) return;
this.updateDirtyInputs(input);
this.toggleSubmission();
}
updateDirtyInputs(input) {
const { name } = input;
const isDirty =
input.dataset.dirtySubmitOriginalValue !== DirtySubmitForm.inputCurrentValue(input);
const indexOfInputName = this.dirtyInputs.indexOf(name);
const isExisting = indexOfInputName !== -1;
if (isDirty && !isExisting) this.dirtyInputs.push(name);
if (!isDirty && isExisting) this.dirtyInputs.splice(indexOfInputName, 1);
}
toggleSubmission() {
this.isDisabled = this.dirtyInputs.length === 0;
this.submits.forEach(element => {
element.disabled = this.isDisabled;
});
}
formSubmit(event) {
if (this.isDisabled) {
event.preventDefault();
event.stopImmediatePropagation();
}
return !this.isDisabled;
}
static initInput(element) {
element.dataset.dirtySubmitOriginalValue = DirtySubmitForm.inputCurrentValue(element);
}
static isInputCheckable(input) {
return input.type === 'checkbox' || input.type === 'radio';
}
static inputCurrentValue(input) {
return DirtySubmitForm.isInputCheckable(input) ? input.checked.toString() : input.value;
}
}
DirtySubmitForm.THROTTLE_DURATION = 500;
export default DirtySubmitForm;

View File

@ -0,0 +1,27 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Flash from '~/flash';
export default function initDismissableCallout(alertSelector) {
const alertEl = document.querySelector(alertSelector);
if (!alertEl) {
return;
}
const closeButtonEl = alertEl.getElementsByClassName('close')[0];
const { dismissEndpoint, featureId } = closeButtonEl.dataset;
closeButtonEl.addEventListener('click', () => {
axios
.post(dismissEndpoint, {
feature_name: featureId,
})
.then(() => {
$(alertEl).alert('close');
})
.catch(() => {
Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
});
});
}

View File

@ -5,6 +5,7 @@ import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
import boardsStore from './boards/stores/boards_store';
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
@ -58,7 +59,7 @@ class DueDateSelect {
$dueDateInput.val(calendar.toString(dateText));
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
boardsStore.detail.issue.dueDate = $dueDateInput.val();
this.updateIssueBoardIssue();
} else {
this.saveDueDate(true);
@ -79,7 +80,7 @@ class DueDateSelect {
calendar.setDate(null);
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
boardsStore.detail.issue.dueDate = '';
this.updateIssueBoardIssue();
} else {
$(`input[name='${this.fieldName}']`).val('');
@ -123,7 +124,7 @@ class DueDateSelect {
this.$loading.fadeOut();
};
gl.issueBoards.BoardsStore.detail.issue
boardsStore.detail.issue
.update(this.$dropdown.attr('data-issue-update'))
.then(fadeOutLoader)
.catch(fadeOutLoader);

View File

@ -60,11 +60,9 @@ export default {
>
<span>
<icon name="play" />
<i
class="fa fa-caret-down"
aria-hidden="true"
>
</i>
<icon
name="chevron-down"
/>
<gl-loading-icon v-if="isLoading" />
</span>
</button>

View File

@ -4,6 +4,7 @@ import _ from 'underscore';
import tooltip from '~/vue_shared/directives/tooltip';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
@ -24,6 +25,7 @@ export default {
components: {
UserAvatarLink,
CommitComponent,
Icon,
ActionsComponent,
ExternalUrlComponent,
StopComponent,
@ -448,6 +450,10 @@ export default {
this.canRetry
);
},
folderIconName() {
return this.model.isOpen ? 'chevron-down' : 'chevron-right';
},
},
methods: {
@ -494,27 +500,15 @@ export default {
role="button"
@click="onClickFolder">
<span class="folder-icon">
<i
v-show="model.isOpen"
class="fa fa-caret-down"
aria-hidden="true"
>
</i>
<i
v-show="!model.isOpen"
class="fa fa-caret-right"
aria-hidden="true"
>
</i>
</span>
<icon
:name="folderIconName"
class="folder-icon"
/>
<span class="folder-icon">
<i
class="fa fa-folder"
aria-hidden="true">
</i>
</span>
<icon
name="folder"
class="folder-icon"
/>
<span>
{{ model.folderName }}

View File

@ -1,10 +1,14 @@
<script>
import _ from 'underscore';
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import frequentItemsMixin from './frequent_items_mixin';
export default {
components: {
Icon,
},
mixins: [frequentItemsMixin],
data() {
return {
@ -45,11 +49,10 @@ export default {
type="search"
class="form-control"
/>
<i
<icon
v-if="!searchQuery"
class="search-icon fa fa-fw fa-search"
aria-hidden="true"
>
</i>
name="search"
class="search-icon"
/>
</div>
</template>

View File

@ -117,7 +117,7 @@ export default {
<button
:disabled="!hasChanges"
type="button"
class="btn btn-primary btn-sm btn-block"
class="btn btn-primary btn-sm btn-block qa-begin-commit-button"
@click="toggleIsSmall"
>
{{ __('Commit…') }}
@ -147,7 +147,7 @@ export default {
<loading-button
:loading="submitCommitLoading"
:label="commitButtonText"
container-class="btn btn-success btn-sm float-left"
container-class="btn btn-success btn-sm float-left qa-commit-button"
@click="commitChanges"
/>
<button

View File

@ -38,14 +38,18 @@ export default {
return this.modifiedFilesLength ? 'multi-file-modified' : '';
},
additionsTooltip() {
return sprintf(n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength), {
type: this.title.toLowerCase(),
count: this.addedFilesLength,
});
return sprintf(
n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength),
{
type: this.title.toLowerCase(),
count: this.addedFilesLength,
},
);
},
modifiedTooltip() {
return sprintf(
n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength), {
n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength),
{
type: this.title.toLowerCase(),
count: this.modifiedFilesLength,
},

View File

@ -25,10 +25,7 @@ export default {
return `discard-file-${this.path}`;
},
modalTitle() {
return sprintf(
__('Discard changes to %{path}?'),
{ path: this.path },
);
return sprintf(__('Discard changes to %{path}?'), { path: this.path });
},
},
methods: {

View File

@ -47,7 +47,7 @@ export default {
</script>
<template>
<div class="d-flex align-items-center ide-file-templates">
<div class="d-flex align-items-center ide-file-templates qa-file-templates-bar">
<strong class="append-right-default">
{{ __('File templates') }}
</strong>
@ -63,7 +63,7 @@ export default {
:is-async-data="true"
:searchable="true"
:title="__('File templates')"
class="mr-2"
class="mr-2 qa-file-template-dropdown"
@click="selectTemplate"
/>
<transition name="fade">

View File

@ -92,7 +92,7 @@ export default {
v-model="search"
:placeholder="__('Filter...')"
type="search"
class="dropdown-input-field"
class="dropdown-input-field qa-dropdown-filter-input"
/>
<i
aria-hidden="true"

View File

@ -24,13 +24,7 @@ export default {
IdeProjectHeader,
},
computed: {
...mapState([
'loading',
'currentActivityView',
'changedFiles',
'stagedFiles',
'lastCommitMsg',
]),
...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']),
...mapGetters(['currentProject', 'someUncommitedChanges']),
showSuccessMessage() {
return (

View File

@ -45,7 +45,7 @@ export default {
<new-entry-button
:label="__('New file')"
:show-label="false"
class="d-flex border-0 p-0 mr-3"
class="d-flex border-0 p-0 mr-3 qa-new-file"
icon="doc-new"
@click="openNewEntryModal({ type: 'blob' })"
/>

View File

@ -43,7 +43,7 @@ export default {
<template>
<div
class="ide-file-list"
class="ide-file-list qa-file-list"
>
<template v-if="showLoading">
<div

View File

@ -37,14 +37,10 @@ export default {
return this.hasSearchFocus && !this.search && !this.currentSearchType;
},
type() {
return this.currentSearchType
? this.currentSearchType.type
: '';
return this.currentSearchType ? this.currentSearchType.type : '';
},
searchTokens() {
return this.currentSearchType
? [this.currentSearchType]
: [];
return this.currentSearchType ? [this.currentSearchType] : [];
},
},
watch: {

View File

@ -13,9 +13,7 @@ export default {
computed: {
...mapState(['currentBranchId', 'currentMergeRequestId']),
mergeRequestLabel() {
return this.currentMergeRequestId
? `!${this.currentMergeRequestId}`
: EMPTY_LABEL;
return this.currentMergeRequestId ? `!${this.currentMergeRequestId}` : EMPTY_LABEL;
},
branchLabel() {
return this.currentBranchId || EMPTY_LABEL;

View File

@ -110,12 +110,12 @@ export default {
ref="fieldName"
v-model="entryName"
type="text"
class="form-control"
class="form-control qa-full-file-path"
placeholder="/dir/file_name"
/>
<ul
v-if="isCreatingNew"
class="prepend-top-default list-inline"
class="prepend-top-default list-inline qa-template-list"
>
<li
v-for="(template, index) in templateTypes"

View File

@ -43,34 +43,25 @@ export default {
{
show: this.currentMergeRequestId,
title: __('Merge Request'),
views: [
rightSidebarViews.mergeRequestInfo,
],
views: [rightSidebarViews.mergeRequestInfo],
icon: 'text-description',
},
{
show: true,
title: __('Pipelines'),
views: [
rightSidebarViews.pipelines,
rightSidebarViews.jobsDetail,
],
views: [rightSidebarViews.pipelines, rightSidebarViews.jobsDetail],
icon: 'rocket',
},
{
show: this.showLivePreview,
title: __('Live preview'),
views: [
rightSidebarViews.clientSidePreview,
],
views: [rightSidebarViews.clientSidePreview],
icon: 'live-preview',
},
];
},
tabs() {
return this.defaultTabs
.concat(this.extensionTabs)
.filter(tab => tab.show);
return this.defaultTabs.concat(this.extensionTabs).filter(tab => tab.show);
},
tabViews() {
return _.flatten(this.tabs.map(tab => tab.views));

View File

@ -25,12 +25,7 @@ export default {
...mapState('rightPane', {
rightPaneIsOpen: 'isOpen',
}),
...mapState([
'rightPanelCollapsed',
'viewer',
'panelResizing',
'currentActivityView',
]),
...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
...mapGetters([
'currentMergeRequest',
'getStagedFile',

View File

@ -30,9 +30,7 @@ export default {
},
computed: {
placeholderText() {
return this.tokens.length
? ''
: this.placeholder;
return this.tokens.length ? '' : this.placeholder;
},
},
watch: {

View File

@ -21,10 +21,7 @@ Vue.use(Translate);
export function initIde(el, options = {}) {
if (!el) return null;
const {
extraInitialData = () => ({}),
rootComponent = ide,
} = options;
const { extraInitialData = () => ({}), rootComponent = ide } = options;
return new Vue({
el,

View File

@ -11,14 +11,16 @@ export const computeDiff = (originalContent, newContent) => {
if (findOnLine) {
Object.assign(findOnLine, change, {
modified: true,
endLineNumber: (lineNumber + change.count) - 1,
endLineNumber: lineNumber + change.count - 1,
});
} else if ('added' in change || 'removed' in change) {
acc.push(Object.assign({}, change, {
lineNumber,
modified: undefined,
endLineNumber: (lineNumber + change.count) - 1,
}));
acc.push(
Object.assign({}, change, {
lineNumber,
modified: undefined,
endLineNumber: lineNumber + change.count - 1,
}),
);
}
if (!change.removed) {

View File

@ -1,7 +1,7 @@
import { computeDiff } from './diff';
// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', (e) => {
self.addEventListener('message', e => {
const { data } = e;
// eslint-disable-next-line no-restricted-globals

View File

@ -116,57 +116,57 @@ export const openMergeRequest = (
targetProjectId,
mergeRequestId,
})
.then(mr => {
dispatch('setCurrentBranchId', mr.source_branch);
.then(mr => {
dispatch('setCurrentBranchId', mr.source_branch);
dispatch('getBranchData', {
projectId,
branchId: mr.source_branch,
});
dispatch('getBranchData', {
projectId,
branchId: mr.source_branch,
});
return dispatch('getFiles', {
projectId,
branchId: mr.source_branch,
});
})
.then(() =>
dispatch('getMergeRequestVersions', {
projectId,
targetProjectId,
mergeRequestId,
}),
)
.then(() =>
dispatch('getMergeRequestChanges', {
projectId,
targetProjectId,
mergeRequestId,
}),
)
.then(mrChanges => {
if (mrChanges.changes.length) {
dispatch('updateActivityBarView', activityBarViews.review);
}
mrChanges.changes.forEach((change, ind) => {
const changeTreeEntry = state.entries[change.new_path];
if (changeTreeEntry) {
dispatch('setFileMrChange', {
file: changeTreeEntry,
mrChange: change,
});
if (ind < 10) {
dispatch('getFileData', {
path: change.new_path,
makeFileActive: ind === 0,
});
}
return dispatch('getFiles', {
projectId,
branchId: mr.source_branch,
});
})
.then(() =>
dispatch('getMergeRequestVersions', {
projectId,
targetProjectId,
mergeRequestId,
}),
)
.then(() =>
dispatch('getMergeRequestChanges', {
projectId,
targetProjectId,
mergeRequestId,
}),
)
.then(mrChanges => {
if (mrChanges.changes.length) {
dispatch('updateActivityBarView', activityBarViews.review);
}
mrChanges.changes.forEach((change, ind) => {
const changeTreeEntry = state.entries[change.new_path];
if (changeTreeEntry) {
dispatch('setFileMrChange', {
file: changeTreeEntry,
mrChange: change,
});
if (ind < 10) {
dispatch('getFileData', {
path: change.new_path,
makeFileActive: ind === 0,
});
}
}
});
})
.catch(e => {
flash(__('Error while loading the merge request. Please try again.'));
throw e;
});
})
.catch(e => {
flash(__('Error while loading the merge request. Please try again.'));
throw e;
});

View File

@ -125,10 +125,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
});
};
export const openBranch = (
{ dispatch, state },
{ projectId, branchId, basePath },
) => {
export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath }) => {
dispatch('setCurrentBranchId', branchId);
dispatch('getBranchData', {
@ -136,23 +133,20 @@ export const openBranch = (
branchId,
});
return (
dispatch('getFiles', {
projectId,
branchId,
})
.then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(state.entries).find(
key => key === path && !state.entries[key].pending,
);
const treeEntry = state.entries[treeEntryKey];
return dispatch('getFiles', {
projectId,
branchId,
}).then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(state.entries).find(
key => key === path && !state.entries[key].pending,
);
const treeEntry = state.entries[treeEntryKey];
if (treeEntry) {
dispatch('handleTreeEntryAction', treeEntry);
}
if (treeEntry) {
dispatch('handleTreeEntryAction', treeEntry);
}
})
);
}
});
};

View File

@ -13,6 +13,7 @@ export default {
},
[types.SET_SELECTED_TEMPLATE_TYPE](state, type) {
state.selectedTemplateType = type;
state.templates = [];
},
[types.SET_UPDATE_SUCCESS](state, success) {
state.updateSuccess = success;

View File

@ -3,8 +3,7 @@ import Api from '../../../../api';
import { scopes } from './constants';
import * as types from './mutation_types';
export const requestMergeRequests = ({ commit }) =>
commit(types.REQUEST_MERGE_REQUESTS);
export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS);
export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => {
dispatch(
'setErrorMessage',

View File

@ -1,281 +1,279 @@
<script>
import Visibility from 'visibilityjs';
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
import Visibility from 'visibilityjs';
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
components: {
descriptionComponent,
titleComponent,
editedComponent,
formComponent,
export default {
components: {
descriptionComponent,
titleComponent,
editedComponent,
formComponent,
},
mixins: [recaptchaModalImplementor],
props: {
endpoint: {
required: true,
type: String,
},
mixins: [
recaptchaModalImplementor,
],
props: {
endpoint: {
required: true,
type: String,
},
updateEndpoint: {
required: true,
type: String,
},
canUpdate: {
required: true,
type: Boolean,
},
canDestroy: {
required: true,
type: Boolean,
},
showInlineEditButton: {
type: Boolean,
required: false,
default: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
issuableRef: {
type: String,
required: true,
},
initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
initialTaskStatus: {
type: String,
required: false,
default: '',
},
updatedAt: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
updatedByPath: {
type: String,
required: false,
default: '',
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
updateEndpoint: {
required: true,
type: String,
},
data() {
const store = new Store({
titleHtml: this.initialTitleHtml,
titleText: this.initialTitleText,
descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
});
canUpdate: {
required: true,
type: Boolean,
},
canDestroy: {
required: true,
type: Boolean,
},
showInlineEditButton: {
type: Boolean,
required: false,
default: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
issuableRef: {
type: String,
required: true,
},
initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
initialTaskStatus: {
type: String,
required: false,
default: '',
},
updatedAt: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
updatedByPath: {
type: String,
required: false,
default: '',
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
data() {
const store = new Store({
titleHtml: this.initialTitleHtml,
titleText: this.initialTitleText,
descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
});
return {
store,
state: store.state,
showForm: false,
};
return {
store,
state: store.state,
showForm: false,
};
},
computed: {
formState() {
return this.store.formState;
},
computed: {
formState() {
return this.store.formState;
},
hasUpdated() {
return !!this.state.updatedAt;
},
issueChanged() {
const descriptionChanged =
this.initialDescriptionText !== this.store.formState.description;
const titleChanged =
this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
hasUpdated() {
return !!this.state.updatedAt;
},
created() {
this.service = new Service(this.endpoint);
this.poll = new Poll({
resource: this.service,
method: 'getData',
successCallback: res => this.store.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
});
issueChanged() {
const descriptionChanged = this.initialDescriptionText !== this.store.formState.description;
const titleChanged = this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
},
created() {
this.service = new Service(this.endpoint);
this.poll = new Poll({
resource: this.service,
method: 'getData',
successCallback: res => this.store.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.makeRequest();
this.poll.restart();
} else {
this.poll.stop();
}
});
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
},
beforeDestroy() {
eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
},
methods: {
handleBeforeUnloadEvent(e) {
const event = e;
if (this.showForm && this.issueChanged) {
event.returnValue = 'Are you sure you want to lose your issue information?';
}
return undefined;
},
beforeDestroy() {
eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
},
methods: {
handleBeforeUnloadEvent(e) {
const event = e;
if (this.showForm && this.issueChanged) {
event.returnValue = 'Are you sure you want to lose your issue information?';
}
return undefined;
},
openForm() {
if (!this.showForm) {
this.showForm = true;
this.store.setFormState({
title: this.state.titleText,
description: this.state.descriptionText,
lockedWarningVisible: false,
updateLoading: false,
});
}
},
closeForm() {
this.showForm = false;
},
updateIssuable() {
return this.service.updateIssuable(this.store.formState)
.then(res => res.data)
.then(data => this.checkForSpam(data))
.then((data) => {
if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
return this.service.getData();
})
.then(res => res.data)
.then((data) => {
this.store.updateState(data);
eventHub.$emit('close.form');
})
.catch((error) => {
if (error && error.name === 'SpamError') {
this.openRecaptcha();
} else {
eventHub.$emit('close.form');
window.Flash(`Error updating ${this.issuableType}`);
}
});
},
closeRecaptchaModal() {
openForm() {
if (!this.showForm) {
this.showForm = true;
this.store.setFormState({
title: this.state.titleText,
description: this.state.descriptionText,
lockedWarningVisible: false,
updateLoading: false,
});
this.closeRecaptcha();
},
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.data)
.then((data) => {
// Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
window.Flash(`Error deleting ${this.issuableType}`);
});
},
}
},
};
closeForm() {
this.showForm = false;
},
updateIssuable() {
return this.service
.updateIssuable(this.store.formState)
.then(res => res.data)
.then(data => this.checkForSpam(data))
.then(data => {
if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
return this.service.getData();
})
.then(res => res.data)
.then(data => {
this.store.updateState(data);
eventHub.$emit('close.form');
})
.catch(error => {
if (error && error.name === 'SpamError') {
this.openRecaptcha();
} else {
eventHub.$emit('close.form');
window.Flash(`Error updating ${this.issuableType}`);
}
});
},
closeRecaptchaModal() {
this.store.setFormState({
updateLoading: false,
});
this.closeRecaptcha();
},
deleteIssuable() {
this.service
.deleteIssuable()
.then(res => res.data)
.then(data => {
// Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
window.Flash(`Error deleting ${this.issuableType}`);
});
},
},
};
</script>
<template>

View File

@ -1,110 +1,105 @@
<script>
import $ from 'jquery';
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
import $ from 'jquery';
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
mixins: [
animateMixin,
recaptchaModalImplementor,
],
export default {
mixins: [animateMixin, recaptchaModalImplementor],
props: {
canUpdate: {
type: Boolean,
required: true,
},
descriptionHtml: {
type: String,
required: true,
},
descriptionText: {
type: String,
required: true,
},
taskStatus: {
type: String,
required: false,
default: '',
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
updateUrl: {
type: String,
required: false,
default: null,
},
props: {
canUpdate: {
type: Boolean,
required: true,
},
data() {
return {
preAnimation: false,
pulseAnimation: false,
};
descriptionHtml: {
type: String,
required: true,
},
watch: {
descriptionHtml() {
this.animateChange();
descriptionText: {
type: String,
required: true,
},
taskStatus: {
type: String,
required: false,
default: '',
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
updateUrl: {
type: String,
required: false,
default: null,
},
},
data() {
return {
preAnimation: false,
pulseAnimation: false,
};
},
watch: {
descriptionHtml() {
this.animateChange();
this.$nextTick(() => {
this.renderGFM();
});
},
taskStatus() {
this.updateTaskStatusText();
},
this.$nextTick(() => {
this.renderGFM();
});
},
mounted() {
this.renderGFM();
taskStatus() {
this.updateTaskStatusText();
},
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
},
mounted() {
this.renderGFM();
this.updateTaskStatusText();
},
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
if (this.canUpdate) {
// eslint-disable-next-line no-new
new TaskList({
dataType: this.issuableType,
fieldName: 'description',
selector: '.detail-page-description',
onSuccess: this.taskListUpdateSuccess.bind(this),
});
}
},
taskListUpdateSuccess(data) {
try {
this.checkForSpam(data);
this.closeRecaptcha();
} catch (error) {
if (error && error.name === 'SpamError') this.openRecaptcha();
}
},
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(
`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ?
's' :
''}`,
);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
if (this.canUpdate) {
// eslint-disable-next-line no-new
new TaskList({
dataType: this.issuableType,
fieldName: 'description',
selector: '.detail-page-description',
onSuccess: this.taskListUpdateSuccess.bind(this),
});
}
},
};
taskListUpdateSuccess(data) {
try {
this.checkForSpam(data);
this.closeRecaptcha();
} catch (error) {
if (error && error.name === 'SpamError') this.openRecaptcha();
}
},
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(
`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`,
);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
},
};
</script>
<template>

View File

@ -1,64 +1,64 @@
<script>
import { __, sprintf } from '~/locale';
import updateMixin from '../mixins/update';
import eventHub from '../event_hub';
import { __, sprintf } from '~/locale';
import updateMixin from '../mixins/update';
import eventHub from '../event_hub';
const issuableTypes = {
issue: __('Issue'),
epic: __('Epic'),
};
const issuableTypes = {
issue: __('Issue'),
epic: __('Epic'),
};
export default {
mixins: [updateMixin],
props: {
canDestroy: {
type: Boolean,
required: true,
},
formState: {
type: Object,
required: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
issuableType: {
type: String,
required: true,
},
export default {
mixins: [updateMixin],
props: {
canDestroy: {
type: Boolean,
required: true,
},
data() {
return {
deleteLoading: false,
};
formState: {
type: Object,
required: true,
},
computed: {
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton;
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
methods: {
closeForm() {
eventHub.$emit('close.form');
},
deleteIssuable() {
const confirmMessage = sprintf(__('%{issuableType} will be removed! Are you sure?'), {
issuableType: issuableTypes[this.issuableType],
});
// eslint-disable-next-line no-alert
if (window.confirm(confirmMessage)) {
this.deleteLoading = true;
issuableType: {
type: String,
required: true,
},
},
data() {
return {
deleteLoading: false,
};
},
computed: {
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton;
},
},
methods: {
closeForm() {
eventHub.$emit('close.form');
},
deleteIssuable() {
const confirmMessage = sprintf(__('%{issuableType} will be removed! Are you sure?'), {
issuableType: issuableTypes[this.issuableType],
});
// eslint-disable-next-line no-alert
if (window.confirm(confirmMessage)) {
this.deleteLoading = true;
eventHub.$emit('delete.issuable');
}
},
eventHub.$emit('delete.issuable');
}
},
};
},
};
</script>
<template>

View File

@ -1,33 +1,33 @@
<script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
timeAgoTooltip,
export default {
components: {
timeAgoTooltip,
},
props: {
updatedAt: {
type: String,
required: false,
default: '',
},
props: {
updatedAt: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
updatedByPath: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
computed: {
hasUpdatedBy() {
return this.updatedByName && this.updatedByPath;
},
updatedByPath: {
type: String,
required: false,
default: '',
},
};
},
computed: {
hasUpdatedBy() {
return this.updatedByName && this.updatedByPath;
},
},
};
</script>
<template>
@ -53,4 +53,3 @@
</span>
</small>
</template>

View File

@ -1,45 +1,45 @@
<script>
import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue';
import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue';
export default {
components: {
markdownField,
export default {
components: {
markdownField,
},
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
mounted() {
this.$refs.textarea.focus();
markdownDocsPath: {
type: String,
required: true,
},
};
markdownVersion: {
type: Number,
required: false,
default: 0,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
},
mounted() {
this.$refs.textarea.focus();
},
};
</script>
<template>

View File

@ -1,46 +1,46 @@
<script>
import $ from 'jquery';
import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
import $ from 'jquery';
import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
export default {
props: {
formState: {
type: Object,
required: true,
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
export default {
props: {
formState: {
type: Object,
required: true,
},
computed: {
issuableTemplatesJson() {
return JSON.stringify(this.issuableTemplates);
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
mounted() {
// Create the editor for the template
const editor = document.querySelector('.detail-page-description .note-textarea') || {};
editor.setValue = (val) => {
this.formState.description = val;
};
editor.getValue = () => this.formState.description;
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
},
computed: {
issuableTemplatesJson() {
return JSON.stringify(this.issuableTemplates);
},
},
mounted() {
// Create the editor for the template
const editor = document.querySelector('.detail-page-description .note-textarea') || {};
editor.setValue = val => {
this.formState.description = val;
};
editor.getValue = () => this.formState.description;
this.issuableTemplate = new IssuableTemplateSelectors({
$dropdowns: $(this.$refs.toggle),
editor,
});
},
};
this.issuableTemplate = new IssuableTemplateSelectors({
$dropdowns: $(this.$refs.toggle),
editor,
});
},
};
</script>
<template>

View File

@ -1,15 +1,15 @@
<script>
import updateMixin from '../../mixins/update';
import updateMixin from '../../mixins/update';
export default {
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
export default {
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
};
},
};
</script>
<template>

View File

@ -1,79 +1,79 @@
<script>
import lockedWarning from './locked_warning.vue';
import titleField from './fields/title.vue';
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue';
import lockedWarning from './locked_warning.vue';
import titleField from './fields/title.vue';
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue';
export default {
components: {
lockedWarning,
titleField,
descriptionField,
descriptionTemplate,
editActions,
export default {
components: {
lockedWarning,
titleField,
descriptionField,
descriptionTemplate,
editActions,
},
props: {
canDestroy: {
type: Boolean,
required: true,
},
props: {
canDestroy: {
type: Boolean,
required: true,
},
formState: {
type: Object,
required: true,
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
issuableType: {
type: String,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
formState: {
type: Object,
required: true,
},
computed: {
hasIssuableTemplates() {
return this.issuableTemplates.length;
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
};
issuableType: {
type: String,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
hasIssuableTemplates() {
return this.issuableTemplates.length;
},
},
};
</script>
<template>

View File

@ -1,11 +1,11 @@
<script>
export default {
computed: {
currentPath() {
return window.location.pathname;
},
export default {
computed: {
currentPath() {
return window.location.pathname;
},
};
},
};
</script>
<template>

View File

@ -25,8 +25,10 @@ export default class Store {
}
stateShouldUpdate(data) {
return this.state.titleText !== data.title_text ||
this.state.descriptionText !== data.description_text;
return (
this.state.titleText !== data.title_text ||
this.state.descriptionText !== data.description_text
);
}
setFormState(state) {

View File

@ -1,30 +1,28 @@
<script>
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
TimeagoTooltip,
export default {
components: {
TimeagoTooltip,
},
mixins: [timeagoMixin],
props: {
artifact: {
type: Object,
required: true,
},
mixins: [
timeagoMixin,
],
props: {
artifact: {
type: Object,
required: true,
},
},
computed: {
isExpired() {
return this.artifact.expired;
},
computed: {
isExpired() {
return this.artifact.expired;
},
// Only when the key is `false` we can render this block
willExpire() {
return this.artifact.expired === false;
},
// Only when the key is `false` we can render this block
willExpire() {
return this.artifact.expired === false;
},
};
},
};
</script>
<template>
<div class="block">

View File

@ -1,26 +1,26 @@
<script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
components: {
ClipboardButton,
export default {
components: {
ClipboardButton,
},
props: {
commit: {
type: Object,
required: true,
},
props: {
commit: {
type: Object,
required: true,
},
mergeRequest: {
type: Object,
required: false,
default: null,
},
isLastBlock: {
type: Boolean,
required: true,
},
mergeRequest: {
type: Object,
required: false,
default: null,
},
};
isLastBlock: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div

View File

@ -1,38 +1,38 @@
<script>
export default {
props: {
illustrationPath: {
type: String,
required: true,
},
illustrationSizeClass: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
content: {
type: String,
required: false,
default: null,
},
action: {
type: Object,
required: false,
default: null,
validator(value) {
return (
value === null ||
(Object.prototype.hasOwnProperty.call(value, 'path') &&
Object.prototype.hasOwnProperty.call(value, 'method') &&
Object.prototype.hasOwnProperty.call(value, 'button_title'))
);
},
export default {
props: {
illustrationPath: {
type: String,
required: true,
},
illustrationSizeClass: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
content: {
type: String,
required: false,
default: null,
},
action: {
type: Object,
required: false,
default: null,
validator(value) {
return (
value === null ||
(Object.prototype.hasOwnProperty.call(value, 'path') &&
Object.prototype.hasOwnProperty.call(value, 'method') &&
Object.prototype.hasOwnProperty.call(value, 'button_title'))
);
},
},
};
},
};
</script>
<template>
<div class="row empty-state">

View File

@ -1,129 +1,131 @@
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale';
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale';
export default {
components: {
CiIcon,
export default {
components: {
CiIcon,
},
props: {
deploymentStatus: {
type: Object,
required: true,
},
props: {
deploymentStatus: {
type: Object,
required: true,
},
iconStatus: {
type: Object,
required: true,
},
iconStatus: {
type: Object,
required: true,
},
computed: {
environment() {
let environmentText;
switch (this.deploymentStatus.status) {
case 'last':
},
computed: {
environment() {
let environmentText;
switch (this.deploymentStatus.status) {
case 'last':
environmentText = sprintf(
__('This job is the most recent deployment to %{link}.'),
{ link: this.environmentLink },
false,
);
break;
case 'out_of_date':
if (this.hasLastDeployment) {
environmentText = sprintf(
__('This job is the most recent deployment to %{link}.'),
{ link: this.environmentLink },
__(
'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`),
},
false,
);
break;
case 'out_of_date':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`),
},
false,
);
} else {
environmentText = sprintf(
__('This job is an out-of-date deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
case 'failed':
} else {
environmentText = sprintf(
__('The deployment of this job to %{environmentLink} did not succeed.'),
__('This job is an out-of-date deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
break;
case 'creating':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink(__('latest deployment')),
},
false,
);
} else {
environmentText = sprintf(
__('This job is creating a deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
default:
break;
}
return environmentText;
},
environmentLink() {
if (this.hasEnvironment) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${
this.deploymentStatus.environment.environment_path
}" class="js-environment-link">`,
name: _.escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
}
break;
case 'failed':
environmentText = sprintf(
__('The deployment of this job to %{environmentLink} did not succeed.'),
{ environmentLink: this.environmentLink },
false,
);
}
return '';
},
hasLastDeployment() {
return this.hasEnvironment && this.deploymentStatus.environment.last_deployment;
},
lastDeployment() {
return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {};
},
hasEnvironment() {
return !_.isEmpty(this.deploymentStatus.environment);
},
lastDeploymentPath() {
return !_.isEmpty(this.lastDeployment.deployable) ? this.lastDeployment.deployable.build_path : '';
},
break;
case 'creating':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink(__('latest deployment')),
},
false,
);
} else {
environmentText = sprintf(
__('This job is creating a deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
default:
break;
}
return environmentText;
},
methods: {
deploymentLink(name) {
environmentLink() {
if (this.hasEnvironment) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`,
name,
startLink: `<a href="${
this.deploymentStatus.environment.environment_path
}" class="js-environment-link">`,
name: _.escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
false,
);
},
}
return '';
},
};
hasLastDeployment() {
return this.hasEnvironment && this.deploymentStatus.environment.last_deployment;
},
lastDeployment() {
return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {};
},
hasEnvironment() {
return !_.isEmpty(this.deploymentStatus.environment);
},
lastDeploymentPath() {
return !_.isEmpty(this.lastDeployment.deployable)
? this.lastDeployment.deployable.build_path
: '';
},
},
methods: {
deploymentLink(name) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`,
name,
endLink: '</a>',
},
false,
);
},
},
};
</script>
<template>
<div class="prepend-top-default js-environment-container">

View File

@ -1,28 +1,28 @@
<script>
import _ from 'underscore';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import _ from 'underscore';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
TimeagoTooltip,
export default {
components: {
TimeagoTooltip,
},
props: {
user: {
type: Object,
required: false,
default: () => ({}),
},
props: {
user: {
type: Object,
required: false,
default: () => ({}),
},
erasedAt: {
type: String,
required: true,
},
erasedAt: {
type: String,
required: true,
},
computed: {
isErasedByUser() {
return !_.isEmpty(this.user);
},
},
computed: {
isErasedByUser() {
return !_.isEmpty(this.user);
},
};
},
};
</script>
<template>
<div class="prepend-top-default js-build-erased">

View File

@ -18,7 +18,7 @@
StuckBlock,
},
props: {
runnerHelpUrl: {
runnerSettingsUrl: {
type: String,
required: false,
default: null,
@ -30,7 +30,7 @@
'headerActions',
'headerTime',
'shouldRenderCalloutMessage',
'jobHasStarted',
'shouldRenderTriggeredLabel',
'hasEnvironment',
'isJobStuck',
'hasTrace',
@ -58,7 +58,7 @@
:user="job.user"
:actions="headerActions"
:has-sidebar-button="true"
:should-render-triggered-label="jobHasStarted"
:should-render-triggered-label="shouldRenderTriggeredLabel"
:item-name="__('Job')"
/>
</div>
@ -76,7 +76,7 @@
class="js-job-stuck"
:has-no-runners-for-project="job.runners.available"
:tags="job.tags"
:runners-path="runnerHelpUrl"
:runners-path="runnerSettingsUrl"
/>
<environments-block
@ -87,8 +87,8 @@
/>
<erased-block
v-if="job.erased"
class="js-job-erased"
v-if="job.erased_at"
class="js-job-erased-block"
:user="job.erased_by"
:erased-at="job.erased_at"
/>

View File

@ -1,17 +1,17 @@
<script>
export default {
name: 'JobLog',
props: {
trace: {
type: String,
required: true,
},
isComplete: {
type: Boolean,
required: true,
},
export default {
name: 'JobLog',
props: {
trace: {
type: String,
required: true,
},
};
isComplete: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<pre class="build-trace">

View File

@ -1,72 +1,71 @@
<script>
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale';
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale';
export default {
components: {
Icon,
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
erasePath: {
type: String,
required: false,
default: null,
},
directives: {
tooltip,
size: {
type: Number,
required: true,
},
props: {
erasePath: {
type: String,
required: false,
default: null,
},
size: {
type: Number,
required: true,
},
rawPath: {
type: String,
required: false,
default: null,
},
isScrollTopDisabled: {
type: Boolean,
required: true,
},
isScrollBottomDisabled: {
type: Boolean,
required: true,
},
isScrollingDown: {
type: Boolean,
required: true,
},
isTraceSizeVisible: {
type: Boolean,
required: true,
},
rawPath: {
type: String,
required: false,
default: null,
},
computed: {
jobLogSize() {
return sprintf('Showing last %{size} of log -', {
size: numberToHumanSize(this.size),
});
},
isScrollTopDisabled: {
type: Boolean,
required: true,
},
mounted() {
polyfillSticky(this.$el);
isScrollBottomDisabled: {
type: Boolean,
required: true,
},
methods: {
handleScrollToTop() {
this.$emit('scrollJobLogTop');
},
handleScrollToBottom() {
this.$emit('scrollJobLogBottom');
},
isScrollingDown: {
type: Boolean,
required: true,
},
};
isTraceSizeVisible: {
type: Boolean,
required: true,
},
},
computed: {
jobLogSize() {
return sprintf('Showing last %{size} of log -', {
size: numberToHumanSize(this.size),
});
},
},
mounted() {
polyfillSticky(this.$el);
},
methods: {
handleScrollToTop() {
this.$emit('scrollJobLogTop');
},
handleScrollToBottom() {
this.$emit('scrollJobLogBottom');
},
},
};
</script>
<template>
<div class="top-bar">
<div class="top-bar affix js-top-bar">
<!-- truncate information -->
<div class="js-truncated-info truncated-info d-none d-sm-block float-left">
<template v-if="isTraceSizeVisible">

View File

@ -1,36 +1,36 @@
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
CiIcon,
Icon,
export default {
components: {
CiIcon,
Icon,
},
directives: {
tooltip,
},
props: {
jobs: {
type: Array,
required: true,
},
directives: {
tooltip,
jobId: {
type: Number,
required: true,
},
props: {
jobs: {
type: Array,
required: true,
},
jobId: {
type: Number,
required: true,
},
},
methods: {
isJobActive(currentJobId) {
return this.jobId === currentJobId;
},
methods: {
isJobActive(currentJobId) {
return this.jobId === currentJobId;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
};
},
};
</script>
<template>
<div class="js-jobs-container builds-container">

View File

@ -1,116 +1,116 @@
<script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import DetailRow from './sidebar_detail_row.vue';
import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import DetailRow from './sidebar_detail_row.vue';
import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
export default {
name: 'JobSidebar',
components: {
ArtifactsBlock,
CommitBlock,
DetailRow,
Icon,
TriggerBlock,
StagesDropdown,
JobsContainer,
export default {
name: 'JobSidebar',
components: {
ArtifactsBlock,
CommitBlock,
DetailRow,
Icon,
TriggerBlock,
StagesDropdown,
JobsContainer,
},
mixins: [timeagoMixin],
props: {
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
mixins: [timeagoMixin],
props: {
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
terminalPath: {
type: String,
required: false,
default: null,
},
terminalPath: {
type: String,
required: false,
default: null,
},
computed: {
...mapState(['job', 'isLoading', 'stages', 'jobs']),
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
let className =
'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
className +=
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
return '';
}
},
computed: {
...mapState(['job', 'isLoading', 'stages', 'jobs', 'selectedStage']),
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
let className =
'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
className +=
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
return '';
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
return t;
},
renderBlock() {
return (
this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path
);
},
hasArtifact() {
return !_.isEmpty(this.job.artifact);
},
hasTriggers() {
return !_.isEmpty(this.job.trigger);
},
hasStages() {
return (
(this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0) ||
false
);
},
commit() {
return this.job.pipeline.commit || {};
},
return t;
},
methods: {
...mapActions(['fetchJobsForStage']),
renderBlock() {
return (
this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path
);
},
};
hasArtifact() {
return !_.isEmpty(this.job.artifact);
},
hasTriggers() {
return !_.isEmpty(this.job.trigger);
},
hasStages() {
return (
(this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0) ||
false
);
},
commit() {
return this.job.pipeline.commit || {};
},
},
methods: {
...mapActions(['fetchJobsForStage']),
},
};
</script>
<template>
<aside
class="right-sidebar right-sidebar-expanded build-sidebar"
class="js-build-sidebar right-sidebar right-sidebar-expanded build-sidebar"
data-offset-top="101"
data-spy="affix"
>
@ -276,6 +276,7 @@
<stages-dropdown
:stages="stages"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
@requestSidebarStageDropdown="fetchJobsForStage"
/>

View File

@ -1,31 +1,31 @@
<script>
export default {
name: 'SidebarDetailRow',
props: {
title: {
type: String,
required: false,
default: '',
},
value: {
type: String,
required: true,
},
helpUrl: {
type: String,
required: false,
default: '',
},
export default {
name: 'SidebarDetailRow',
props: {
title: {
type: String,
required: false,
default: '',
},
computed: {
hasTitle() {
return this.title.length > 0;
},
hasHelpURL() {
return this.helpUrl.length > 0;
},
value: {
type: String,
required: true,
},
};
helpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
hasTitle() {
return this.title.length > 0;
},
hasHelpURL() {
return this.helpUrl.length > 0;
},
},
};
</script>
<template>
<p class="build-detail-row">

View File

@ -1,50 +1,39 @@
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
CiIcon,
Icon,
export default {
components: {
CiIcon,
Icon,
},
props: {
pipeline: {
type: Object,
required: true,
},
props: {
pipeline: {
type: Object,
required: true,
},
stages: {
type: Array,
required: true,
},
stages: {
type: Array,
required: true,
},
data() {
return {
selectedStage: this.stages.length > 0 ? this.stages[0].name : __('More'),
};
selectedStage: {
type: String,
required: true,
},
computed: {
hasRef() {
return !_.isEmpty(this.pipeline.ref);
},
},
computed: {
hasRef() {
return !_.isEmpty(this.pipeline.ref);
},
watch: {
// When the component is initially mounted it may start with an empty stages array.
// Once the prop is updated, we set the first stage as the selected one
stages(newVal) {
if (newVal.length) {
this.selectedStage = newVal[0].name;
}
},
},
methods: {
onStageClick(stage) {
this.$emit('requestSidebarStageDropdown', stage);
},
methods: {
onStageClick(stage) {
this.$emit('requestSidebarStageDropdown', stage);
this.selectedStage = stage.name;
},
},
};
},
};
</script>
<template>
<div class="block-last dropdown">

View File

@ -1,27 +1,27 @@
<script>
export default {
props: {
trigger: {
type: Object,
required: true,
},
export default {
props: {
trigger: {
type: Object,
required: true,
},
data() {
return {
areVariablesVisible: false,
};
},
data() {
return {
areVariablesVisible: false,
};
},
computed: {
hasVariables() {
return this.trigger.variables && this.trigger.variables.length > 0;
},
computed: {
hasVariables() {
return this.trigger.variables && this.trigger.variables.length > 0;
},
},
methods: {
revealVariables() {
this.areVariablesVisible = true;
},
methods: {
revealVariables() {
this.areVariablesVisible = true;
},
},
};
},
};
</script>
<template>

View File

@ -9,8 +9,7 @@ import createStore from './store';
export default () => {
const { dataset } = document.getElementById('js-job-details-vue');
// eslint-disable-next-line no-new
new Job();
const store = createStore();
store.dispatch('setJobEndpoint', dataset.endpoint);
@ -33,7 +32,7 @@ export default () => {
props: {
isLoading: this.isLoading,
job: this.job,
runnerHelpUrl: dataset.runnerHelpUrl,
runnerSettingsUrl: dataset.runnerSettingsUrl,
},
});
},
@ -71,4 +70,7 @@ export default () => {
});
},
});
// eslint-disable-next-line no-new
new Job();
};

View File

@ -139,10 +139,12 @@ export const fetchStages = ({ state, dispatch }) => {
dispatch('requestStages');
axios
.get(state.job.pipeline.path)
.get(`${state.job.pipeline.path}.json`)
.then(({ data }) => {
// Set selected stage
dispatch('receiveStagesSuccess', data.details.stages);
dispatch('fetchJobsForStage', data.details.stages[0]);
const selectedStage = data.details.stages.find(stage => stage.name === state.selectedStage);
dispatch('fetchJobsForStage', selectedStage);
})
.catch(() => dispatch('receiveStagesError'));
};
@ -156,11 +158,12 @@ export const receiveStagesError = ({ commit }) => {
/**
* Jobs list on sidebar - depend on stages dropdown
*/
export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE);
export const requestJobsForStage = ({ commit }, stage) =>
commit(types.REQUEST_JOBS_FOR_STAGE, stage);
// On stage click, set selected stage + fetch job
export const fetchJobsForStage = ({ dispatch }, stage) => {
dispatch('requestJobsForStage');
dispatch('requestJobsForStage', stage);
axios
.get(stage.dropdown_path, {

View File

@ -22,10 +22,10 @@ export const shouldRenderCalloutMessage = state =>
!_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message);
/**
* When job has not started the key will be `false`
* When job has not started the key will be null
* When job started the key will be a string with a date.
*/
export const jobHasStarted = state => !(state.job.started === false);
export const shouldRenderTriggeredLabel = state => _.isString(state.job.started);
export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);

View File

@ -7,9 +7,10 @@ import mutations from './mutations';
Vue.use(Vuex);
export default () => new Vuex.Store({
actions,
mutations,
getters,
state: state(),
});
export default () =>
new Vuex.Store({
actions,
mutations,
getters,
state: state(),
});

View File

@ -53,6 +53,16 @@ export default {
state.isLoading = false;
state.hasError = false;
state.job = job;
/**
* We only update it on the first request
* The dropdown can be changed by the user
* after the first request,
* and we do not want to hijack that
*/
if (state.selectedStage === 'More' && job.stage) {
state.selectedStage = job.stage;
}
},
[types.RECEIVE_JOB_ERROR](state) {
state.isLoading = false;
@ -81,8 +91,9 @@ export default {
state.stages = [];
},
[types.REQUEST_JOBS_FOR_STAGE](state) {
[types.REQUEST_JOBS_FOR_STAGE](state, stage) {
state.isLoadingJobs = true;
state.selectedStage = stage.name;
},
[types.RECEIVE_JOBS_FOR_STAGE_SUCCESS](state, jobs) {
state.isLoadingJobs = false;

View File

@ -1,3 +1,5 @@
import { __ } from '~/locale';
export default () => ({
jobEndpoint: null,
traceEndpoint: null,
@ -34,7 +36,7 @@ export default () => ({
// sidebar dropdown
isLoadingStages: false,
isLoadingJobs: false,
selectedStage: null,
selectedStage: __('More'),
stages: [],
jobs: [],
});

View File

@ -11,6 +11,7 @@ import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
import flash from './flash';
import ModalStore from './boards/stores/modal_store';
import boardsStore from './boards/stores/boards_store';
export default class LabelsSelect {
constructor(els, options = {}) {
@ -378,7 +379,7 @@ export default class LabelsSelect {
}
else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if ($el.hasClass('is-active')) {
gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
boardsStore.detail.issue.labels.push(new ListLabel({
id: label.id,
title: label.title,
color: label.color[0],
@ -386,16 +387,16 @@ export default class LabelsSelect {
}));
}
else {
var { labels } = gl.issueBoards.BoardsStore.detail.issue;
var { labels } = boardsStore.detail.issue;
labels = labels.filter(function (selectedLabel) {
return selectedLabel.id !== label.id;
});
gl.issueBoards.BoardsStore.detail.issue.labels = labels;
boardsStore.detail.issue.labels = labels;
}
$loading.fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
boardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(fadeOutLoader)
.catch(fadeOutLoader);
}

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