Commit 632b87a8 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' into per-project-pipeline-iid

parents 07d1d8bd 0eb74426
...@@ -9,3 +9,4 @@ lib/gitlab/gitaly_client/operation_service.rb ...@@ -9,3 +9,4 @@ lib/gitlab/gitaly_client/operation_service.rb
lib/gitlab/background_migration/* lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb lib/gitlab/workhorse.rb
lib/gitlab/ci/trace/chunked_io.rb
...@@ -68,6 +68,8 @@ eslint-report.html ...@@ -68,6 +68,8 @@ eslint-report.html
/shared/* /shared/*
/.gitlab_workhorse_secret /.gitlab_workhorse_secret
/webpack-report/ /webpack-report/
/knapsack/
/rspec_flaky/
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/locale/**/*.time_stamp /locale/**/*.time_stamp
/.rspec /.rspec
......
...@@ -28,11 +28,11 @@ Set the title to: `[Security] Description of the original issue` ...@@ -28,11 +28,11 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager. - [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/process.md#secpick-script [seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
#### Documentation and final details #### Documentation and final details
- [ ] Check the topic on #security to see when the next release is going ot happen and add a link to the [links section](#links) - [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details) - [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details) - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details) - [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
......
...@@ -9,6 +9,10 @@ terms. ...@@ -9,6 +9,10 @@ terms.
[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md) [DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
All Documentation content that resides under the [doc/ directory](/doc) of this
repository is licensed under Creative Commons:
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
_This notice should stay as the first item in the CONTRIBUTING.md file._ _This notice should stay as the first item in the CONTRIBUTING.md file._
--- ---
......
...@@ -33,7 +33,7 @@ gem 'grape-route-helpers', '~> 2.1.0' ...@@ -33,7 +33,7 @@ gem 'grape-route-helpers', '~> 2.1.0'
gem 'faraday', '~> 0.12' gem 'faraday', '~> 0.12'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.4'
gem 'doorkeeper', '~> 4.3' gem 'doorkeeper', '~> 4.3'
gem 'doorkeeper-openid_connect', '~> 1.3' gem 'doorkeeper-openid_connect', '~> 1.3'
gem 'omniauth', '~> 1.8' gem 'omniauth', '~> 1.8'
...@@ -41,7 +41,7 @@ gem 'omniauth-auth0', '~> 2.0.0' ...@@ -41,7 +41,7 @@ gem 'omniauth-auth0', '~> 2.0.0'
gem 'omniauth-azure-oauth2', '~> 0.0.9' gem 'omniauth-azure-oauth2', '~> 0.0.9'
gem 'omniauth-cas3', '~> 1.1.4' gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-github', '~> 1.3'
gem 'omniauth-gitlab', '~> 1.0.2' gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.3' gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
...@@ -90,7 +90,7 @@ gem 'github-linguist', '~> 5.3.3', require: 'linguist' ...@@ -90,7 +90,7 @@ gem 'github-linguist', '~> 5.3.3', require: 'linguist'
# API # API
gem 'grape', '~> 1.0' gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.6.0' gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors' gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# Disable strong_params so that Mash does not respond to :permitted? # Disable strong_params so that Mash does not respond to :permitted?
...@@ -416,7 +416,7 @@ group :ed25519 do ...@@ -416,7 +416,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0' gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
......
...@@ -143,7 +143,7 @@ GEM ...@@ -143,7 +143,7 @@ GEM
connection_pool (2.2.1) connection_pool (2.2.1)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.3) crass (1.0.4)
creole (0.5.0) creole (0.5.0)
css_parser (1.5.0) css_parser (1.5.0)
addressable addressable
...@@ -162,10 +162,10 @@ GEM ...@@ -162,10 +162,10 @@ GEM
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.0) device_detector (1.0.0)
devise (4.2.0) devise (4.4.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.1) railties (>= 4.1.0, < 6.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (3.0.0) devise-two-factor (3.0.0)
...@@ -291,7 +291,7 @@ GEM ...@@ -291,7 +291,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.97.0) gitaly-proto (0.99.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -366,8 +366,8 @@ GEM ...@@ -366,8 +366,8 @@ GEM
rack (>= 1.3.0) rack (>= 1.3.0)
rack-accept rack-accept
virtus (>= 1.0.0) virtus (>= 1.0.0)
grape-entity (0.6.0) grape-entity (0.7.1)
activesupport activesupport (>= 4.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grape-route-helpers (2.1.0) grape-route-helpers (2.1.0)
activesupport activesupport
...@@ -546,9 +546,9 @@ GEM ...@@ -546,9 +546,9 @@ GEM
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-facebook (4.0.0) omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2) omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2) omniauth-github (1.3.0)
omniauth (~> 1.0) omniauth (~> 1.5)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.2) omniauth-gitlab (1.0.2)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0) omniauth-oauth2 (~> 1.0)
...@@ -646,7 +646,7 @@ GEM ...@@ -646,7 +646,7 @@ GEM
pry (>= 0.9.10) pry (>= 0.9.10)
public_suffix (3.0.2) public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.9) rack (1.6.10)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
rack-attack (4.4.1) rack-attack (4.4.1)
...@@ -694,7 +694,7 @@ GEM ...@@ -694,7 +694,7 @@ GEM
rainbow (2.2.2) rainbow (2.2.2)
rake rake
raindrops (0.18.0) raindrops (0.18.0)
rake (12.3.0) rake (12.3.1)
rb-fsevent (0.10.2) rb-fsevent (0.10.2)
rb-inotify (0.9.10) rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2) ffi (>= 0.5.0, < 2)
...@@ -735,8 +735,9 @@ GEM ...@@ -735,8 +735,9 @@ GEM
declarative-option (< 0.2.0) declarative-option (< 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
request_store (1.3.1) request_store (1.3.1)
responders (2.3.0) responders (2.4.0)
railties (>= 4.2.0, < 5.1) actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
rest-client (2.0.2) rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
...@@ -966,7 +967,7 @@ GEM ...@@ -966,7 +967,7 @@ GEM
descendants_tracker (~> 0.0, >= 0.0.3) descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9) equalizer (~> 0.0, >= 0.0.9)
vmstat (2.3.0) vmstat (2.3.0)
warden (1.2.6) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (2.3.2) webmock (2.3.2)
addressable (>= 2.3.6) addressable (>= 2.3.6)
...@@ -1028,7 +1029,7 @@ DEPENDENCIES ...@@ -1028,7 +1029,7 @@ DEPENDENCIES
deckar01-task_list (= 2.0.0) deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0) default_value_for (~> 3.0.0)
device_detector device_detector
devise (~> 4.2) devise (~> 4.4)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
doorkeeper (~> 4.3) doorkeeper (~> 4.3)
...@@ -1059,7 +1060,7 @@ DEPENDENCIES ...@@ -1059,7 +1060,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.97.0) gitaly-proto (~> 0.99.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
...@@ -1072,7 +1073,7 @@ DEPENDENCIES ...@@ -1072,7 +1073,7 @@ DEPENDENCIES
google-protobuf (= 3.5.1) google-protobuf (= 3.5.1)
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.11.0) grpc (~> 1.11.0)
...@@ -1113,7 +1114,7 @@ DEPENDENCIES ...@@ -1113,7 +1114,7 @@ DEPENDENCIES
omniauth-azure-oauth2 (~> 0.0.9) omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4) omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0) omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1) omniauth-github (~> 1.3)
omniauth-gitlab (~> 1.0.2) omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3) omniauth-google-oauth2 (~> 0.5.3)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
......
...@@ -162,6 +162,7 @@ GEM ...@@ -162,6 +162,7 @@ GEM
activerecord (>= 3.2.0, < 5.2) activerecord (>= 3.2.0, < 5.2)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.1)
devise (4.4.1) devise (4.4.1)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
...@@ -375,7 +376,7 @@ GEM ...@@ -375,7 +376,7 @@ GEM
rake rake
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
grpc (1.10.0) grpc (1.11.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7) googleauth (>= 0.5.1, < 0.7)
...@@ -554,9 +555,6 @@ GEM ...@@ -554,9 +555,6 @@ GEM
jwt (>= 1.5) jwt (>= 1.5)
omniauth (>= 1.1.1) omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5) omniauth-oauth2 (>= 1.5)
omniauth-jwt (0.0.2)
jwt
omniauth (~> 1.1)
omniauth-kerberos (0.3.0) omniauth-kerberos (0.3.0)
omniauth-multipassword omniauth-multipassword
timfel-krb5-auth (~> 0.8) timfel-krb5-auth (~> 0.8)
...@@ -1033,6 +1031,7 @@ DEPENDENCIES ...@@ -1033,6 +1031,7 @@ DEPENDENCIES
database_cleaner (~> 1.5.0) database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0) deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.5) default_value_for (~> 3.0.5)
device_detector
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
...@@ -1080,7 +1079,7 @@ DEPENDENCIES ...@@ -1080,7 +1079,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.10.0) grpc (~> 1.11.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
hashie-forbidden_attributes hashie-forbidden_attributes
...@@ -1121,7 +1120,6 @@ DEPENDENCIES ...@@ -1121,7 +1120,6 @@ DEPENDENCIES
omniauth-github (~> 1.1.1) omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2) omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3) omniauth-google-oauth2 (~> 0.5.3)
omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
......
...@@ -4,4 +4,9 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of ...@@ -4,4 +4,9 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
---
All Documentation content that resides under the doc/ directory of this
repository is licensed under Creative Commons: CC BY-SA 4.0.
import Flash from '../flash'; import createFlash from '~/flash';
import { s__ } from '../locale'; import { __ } from '~/locale';
import setupToggleButtons from '../toggle_buttons'; import setupToggleButtons from '~/toggle_buttons';
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
export default () => { export default () => {
const clusterList = document.querySelector('.js-clusters-list'); const clusterList = document.querySelector('.js-clusters-list');
gcpSignupOffer();
// The empty state won't have a clusterList // The empty state won't have a clusterList
if (clusterList) { if (clusterList) {
setupToggleButtons( setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
document.querySelector('.js-clusters-list'), ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
(value, toggle) => err => {
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }) createFlash(__('Something went wrong on our end.'));
.catch((err) => { throw err;
Flash(s__('ClusterIntegration|Something went wrong on our end.')); },
throw err; ),
}),
); );
} }
}; };
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Flash from '~/flash';
export default function gcpSignupOffer() {
const alertEl = document.querySelector('.gcp-signup-offer');
if (!alertEl) {
return;
}
const closeButtonEl = alertEl.getElementsByClassName('close')[0];
const { dismissEndpoint, featureId } = closeButtonEl.dataset;
closeButtonEl.addEventListener('click', () => {
axios
.post(dismissEndpoint, {
feature_name: featureId,
})
.then(() => {
$(alertEl).alert('close');
})
.catch(() => {
Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
});
});
}
<script> <script>
import eventHub from '../eventhub'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import eventHub from '../eventhub';
export default { export default {
components: { components: {
loadingIcon, loadingIcon,
},
props: {
deployKey: {
type: Object,
required: true,
}, },
props: { type: {
deployKey: { type: String,
type: Object, required: true,
required: true,
},
type: {
type: String,
required: true,
},
btnCssClass: {
type: String,
required: false,
default: 'btn-default',
},
}, },
data() { btnCssClass: {
return { type: String,
isLoading: false, required: false,
}; default: 'btn-default',
}, },
computed: { },
text() { data() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`; return {
}, isLoading: false,
}, };
methods: { },
doAction() { methods: {
this.isLoading = true; doAction() {
this.isLoading = true;
eventHub.$emit(`${this.type}.key`, this.deployKey, () => { eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
this.isLoading = false; this.isLoading = false;
}); });
},
}, },
}; },
};
</script> </script>
<template> <template>
<button <button
class="btn btn-sm prepend-left-10" class="btn"
:class="[{ disabled: isLoading }, btnCssClass]" :class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading" :disabled="isLoading"
@click="doAction"> @click="doAction">
{{ text }} <slot></slot>
<loading-icon <loading-icon
v-if="isLoading" v-if="isLoading"
:inline="true" :inline="true"
......
<script> <script>
import Flash from '../../flash'; import { s__ } from '~/locale';
import eventHub from '../eventhub'; import Flash from '~/flash';
import DeployKeysService from '../service'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import DeployKeysStore from '../store'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import keysPanel from './keys_panel.vue'; import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
export default { export default {
components: { components: {
keysPanel, KeysPanel,
loadingIcon, LoadingIcon,
NavigationTabs,
},
props: {
endpoint: {
type: String,
required: true,
}, },
props: { projectId: {
endpoint: { type: String,
type: String, required: true,
required: true,
},
}, },
data() { },
return { data() {
isLoading: false, return {
store: new DeployKeysStore(), currentTab: 'enabled_keys',
}; isLoading: false,
store: new DeployKeysStore(),
};
},
scopes: {
enabled_keys: s__('DeployKeys|Enabled deploy keys'),
available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
},
computed: {
tabs() {
return Object.keys(this.$options.scopes).map(scope => {
const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null;
return {
name: this.$options.scopes[scope],
scope,
isActive: scope === this.currentTab,
count,
};
});
},
hasKeys() {
return Object.keys(this.keys).length;
}, },
computed: { keys() {
hasKeys() { return this.store.keys;
return Object.keys(this.keys).length;
},
keys() {
return this.store.keys;
},
}, },
created() { },
this.service = new DeployKeysService(this.endpoint); created() {
this.service = new DeployKeysService(this.endpoint);
eventHub.$on('enable.key', this.enableKey); eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey); eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey); eventHub.$on('disable.key', this.disableKey);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
},
methods: {
onChangeTab(tab) {
this.currentTab = tab;
}, },
mounted() { fetchKeys() {
this.fetchKeys(); this.isLoading = true;
return this.service
.getKeys()
.then(data => {
this.isLoading = false;
this.store.keys = data;
})
.catch(() => {
this.isLoading = false;
this.store.keys = {};
return new Flash(s__('DeployKeys|Error getting deploy keys'));
});
}, },
beforeDestroy() { enableKey(deployKey) {
eventHub.$off('enable.key', this.enableKey); this.service
eventHub.$off('remove.key', this.disableKey); .enableKey(deployKey.id)
eventHub.$off('disable.key', this.disableKey); .then(this.fetchKeys)
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
}, },
methods: { disableKey(deployKey, callback) {
fetchKeys() { // eslint-disable-next-line no-alert
this.isLoading = true; if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
this.service
this.service.getKeys() .disableKey(deployKey.id)
.then((data) => { .then(this.fetchKeys)
this.isLoading = false; .then(callback)
this.store.keys = data; .catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
}) } else {
.catch(() => new Flash('Error getting deploy keys')); callback();
}, }
enableKey(deployKey) {
this.service.enableKey(deployKey.id)
.then(() => this.fetchKeys())
.catch(() => new Flash('Error enabling deploy key'));
},
disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
if (confirm('You are going to remove this deploy key. Are you sure?')) {
this.service.disableKey(deployKey.id)
.then(() => this.fetchKeys())
.then(callback)
.catch(() => new Flash('Error removing deploy key'));
} else {
callback();
}
},
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -82,29 +117,38 @@ ...@@ -82,29 +117,38 @@
<loading-icon <loading-icon
v-if="isLoading && !hasKeys" v-if="isLoading && !hasKeys"
size="2" size="2"
label="Loading deploy keys" :label="s__('DeployKeys|Loading deploy keys')"
/> />
<div v-else-if="hasKeys"> <template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
<div class="fade-left">
<i
class="fa fa-angle-left"
aria-hidden="true"
>
</i>
</div>
<div class="fade-right">
<i
class="fa fa-angle-right"
aria-hidden="true"
>
</i>
</div>
<navigation-tabs
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="deployKeys"
/>
</div>
<keys-panel <keys-panel
title="Enabled deploy keys for this project"
class="qa-project-deploy-keys" class="qa-project-deploy-keys"
:keys="keys.enabled_keys" :project-id="projectId"
:store="store" :keys="keys[currentTab]"
:endpoint="endpoint"
/>
<keys-panel
title="Deploy keys from projects you have access to"
:keys="keys.available_project_keys"
:store="store"
:endpoint="endpoint"
/>
<keys-panel
v-if="keys.public_keys.length"
title="Public deploy keys available to any project"
:keys="keys.public_keys"
:store="store" :store="store"
:endpoint="endpoint" :endpoint="endpoint"
/> />
</div> </template>
</div> </div>
</template> </template>
<script> <script>
import actionBtn from './action_btn.vue'; import _ from 'underscore';
import { getTimeago } from '../../lib/utils/datetime_utility'; import { s__, sprintf } from '~/locale';
import tooltip from '../../vue_shared/directives/tooltip'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default { import actionBtn from './action_btn.vue';
components: {
actionBtn, export default {
}, components: {
directives: { actionBtn,
tooltip, icon,
}, },
props: { directives: {
deployKey: { tooltip,
type: Object, },
required: true, mixins: [timeagoMixin],
}, props: {
store: { deployKey: {
type: Object, type: Object,
required: true, required: true,
}, },
endpoint: { store: {
type: String, type: Object,
required: true, required: true,
}, },
}, endpoint: {
computed: { type: String,
timeagoDate() { required: true,
return getTimeago().format(this.deployKey.created_at); },
}, projectId: {
editDeployKeyPath() { type: String,
return `${this.endpoint}/${this.deployKey.id}/edit`; required: false,
}, default: null,
}, },
methods: { },
isEnabled(id) { data() {
return this.store.findEnabledKey(id) !== undefined; return {
}, projectsExpanded: false,
tooltipTitle(project) { };
return project.can_push ? 'Write access allowed' : 'Read access only'; },
}, computed: {
}, editDeployKeyPath() {
}; return `${this.endpoint}/${this.deployKey.id}/edit`;
},
projects() {
const projects = [...this.deployKey.deploy_keys_projects];
if (this.projectId !== null) {
const indexOfCurrentProject = _.findIndex(
projects,
project =>
project &&
project.project &&
project.project.id &&
project.project.id.toString() === this.projectId,
);
if (indexOfCurrentProject > -1) {
const currentProject = projects.splice(indexOfCurrentProject, 1);
currentProject[0].project.full_name = s__('DeployKeys|Current project');
return currentProject.concat(projects);
}
}
return projects;
},
firstProject() {
return _.head(this.projects);
},
restProjects() {
return _.tail(this.projects);
},
restProjectsTooltip() {
return sprintf(s__('DeployKeys|Expand %{count} other projects'), {
count: this.restProjects.length,
});
},
restProjectsLabel() {
return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length });
},
isEnabled() {
return this.store.isEnabled(this.deployKey.id);
},
isRemovable() {
return (
this.store.isEnabled(this.deployKey.id) &&
this.deployKey.destroyed_when_orphaned &&
this.deployKey.almost_orphaned
);
},
isExpandable() {
return !this.projectsExpanded && this.restProjects.length > 1;
},
isExpanded() {
return this.projectsExpanded || this.restProjects.length === 1;
},
},
methods: {
projectTooltipTitle(project) {
return project.can_push
? s__('DeployKeys|Write access allowed')
: s__('DeployKeys|Read access only');
},
toggleExpanded() {
this.projectsExpanded = !this.projectsExpanded;
},
},
};
</script> </script>
<template> <template>
<div> <div class="gl-responsive-table-row deploy-key">
<div class="pull-left append-right-10 hidden-xs"> <div class="table-section section-40">
<i <div
aria-hidden="true" role="rowheader"
class="fa fa-key key-icon" class="table-mobile-header">
> {{ s__('DeployKeys|Deploy key') }}
</i> </div>
<div class="table-mobile-content">
<strong class="title qa-key-title">
{{ deployKey.title }}
</strong>
<div class="fingerprint qa-key-fingerprint">
{{ deployKey.fingerprint }}
</div>
</div>
</div> </div>
<div class="deploy-key-content key-list-item-info"> <div class="table-section section-30 section-wrap">
<strong class="title qa-key-title"> <div
{{ deployKey.title }} role="rowheader"
</strong> class="table-mobile-header">
<div class="description qa-key-fingerprint"> {{ s__('DeployKeys|Project usage') }}
{{ deployKey.fingerprint }} </div>
<div class="table-mobile-content deploy-project-list">
<template v-if="projects.length > 0">
<a
class="label deploy-project-label"
:title="projectTooltipTitle(firstProject)"
v-tooltip
>
<span>
{{ firstProject.project.full_name }}
</span>
<icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
</a>
<a
v-if="isExpandable"
class="label deploy-project-label"
@click="toggleExpanded"
:title="restProjectsTooltip"
v-tooltip
>
<span>{{ restProjectsLabel }}</span>
</a>
<a
v-else-if="isExpanded"
v-for="deployKeysProject in restProjects"
:key="deployKeysProject.project.full_path"
class="label deploy-project-label"
:href="deployKeysProject.project.full_path"
:title="projectTooltipTitle(deployKeysProject)"
v-tooltip
>
<span>
{{ deployKeysProject.project.full_name }}
</span>
<icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/>
</a>
</template>
<span
v-else
class="text-secondary">{{ __('None') }}</span>
</div> </div>
</div> </div>
<div class="deploy-key-content prepend-left-default deploy-key-projects"> <div class="table-section section-15 text-right">
<a <div
v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects" role="rowheader"
:key="i" class="table-mobile-header">
class="label deploy-project-label" {{ __('Created') }}
:href="deployKeysProject.project.full_path" </div>
:title="tooltipTitle(deployKeysProject)" <div class="table-mobile-content text-secondary key-created-at">
v-tooltip <span
> :title="tooltipTitle(deployKey.created_at)"
{{ deployKeysProject.project.full_name }} v-tooltip>
<i <icon name="calendar"/>
v-if="!deployKeysProject.can_push" <span>{{ timeFormated(deployKey.created_at) }}</span>
aria-hidden="true" </span>
class="fa fa-lock" </div>
>
</i>
</a>
</div> </div>
<div class="deploy-key-content"> <div class="table-section section-15 table-button-footer deploy-key-actions">
<span class="key-created-at"> <div class="btn-group table-action-buttons">
created {{ timeagoDate }} <action-btn
</span> v-if="!isEnabled"
<a :deploy-key="deployKey"
v-if="deployKey.can_edit" type="enable"
class="btn btn-sm" >
:href="editDeployKeyPath" {{ __('Enable') }}
> </action-btn>
Edit <a
</a> v-if="deployKey.can_edit"
<action-btn class="btn btn-default text-secondary"
v-if="!isEnabled(deployKey.id)" :href="editDeployKeyPath"
:deploy-key="deployKey" :title="__('Edit')"
type="enable" data-container="body"
/> v-tooltip
<action-btn >
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned" <icon name="pencil"/>
:deploy-key="deployKey" </a>
btn-css-class="btn-warning" <action-btn
type="remove" v-if="isRemovable"
/> :deploy-key="deployKey"
<action-btn btn-css-class="btn-danger"
v-else type="remove"
:deploy-key="deployKey" :title="__('Remove')"
btn-css-class="btn-warning" data-container="body"
type="disable" v-tooltip
/> >
<icon name="remove"/>
</action-btn>
<action-btn
v-else-if="isEnabled"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable"
:title="__('Disable')"
data-container="body"
v-tooltip
>
<icon name="cancel"/>
</action-btn>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import key from './key.vue'; import deployKey from './key.vue';
export default { export default {
components: { components: {
key, deployKey,
},
props: {
keys: {
type: Array,
required: true,
}, },
props: { store: {
title: { type: Object,
type: String, required: true,
required: true,
},
keys: {
type: Array,
required: true,
},
showHelpBox: {
type: Boolean,
required: false,
default: true,
},
store: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
}, },
}; endpoint: {
type: String,
required: true,
},
projectId: {
type: String,
required: false,
default: null,
},
},
};
</script> </script>
<template> <template>
<div class="deploy-keys-panel"> <div class="deploy-keys-panel table-holder">
<h5> <template v-if="keys.length > 0">
{{ title }} <div
({{ keys.length }}) role="row"
</h5> class="gl-responsive-table-row table-row-header">
<ul <div
class="well-list" role="rowheader"
v-if="keys.length" class="table-section section-40">
> {{ s__('DeployKeys|Deploy key') }}
<li </div>
<div
role="rowheader"
class="table-section section-30">
{{ s__('DeployKeys|Project usage') }}
</div>
<div
role="rowheader"
class="table-section section-15 text-right">
{{ __('Created') }}
</div>
</div>
<deploy-key
v-for="deployKey in keys" v-for="deployKey in keys"
:key="deployKey.id" :key="deployKey.id"
> :deploy-key="deployKey"
<key :store="store"
:deploy-key="deployKey" :endpoint="endpoint"
:store="store" :project-id="projectId"
:endpoint="endpoint" />
/> </template>
</li>
</ul>
<div <div
class="settings-message text-center" class="settings-message text-center"
v-else-if="showHelpBox" v-else
> >
No deploy keys found. Create one with the form above. {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div> </div>
</div> </div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import deployKeysApp from './components/app.vue'; import deployKeysApp from './components/app.vue';
export default () => new Vue({ export default () =>
el: document.getElementById('js-deploy-keys'), new Vue({
components: { el: document.getElementById('js-deploy-keys'),
deployKeysApp, components: {
}, deployKeysApp,
data() { },
return { data() {
endpoint: this.$options.el.dataset.endpoint, return {
}; endpoint: this.$options.el.dataset.endpoint,
}, projectId: this.$options.el.dataset.projectId,
render(createElement) { };
return createElement('deploy-keys-app', { },
props: { render(createElement) {
endpoint: this.endpoint, return createElement('deploy-keys-app', {
}, props: {
}); endpoint: this.endpoint,
}, projectId: this.projectId,
}); },
});
},
});
...@@ -7,21 +7,24 @@ export default class DeployKeysService { ...@@ -7,21 +7,24 @@ export default class DeployKeysService {
constructor(endpoint) { constructor(endpoint) {
this.endpoint = endpoint; this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, { this.resource = Vue.resource(
enable: { `${this.endpoint}{/id}`,
method: 'PUT', {},
url: `${this.endpoint}{/id}/enable`, {
enable: {
method: 'PUT',
url: `${this.endpoint}{/id}/enable`,
},
disable: {
method: 'PUT',
url: `${this.endpoint}{/id}/disable`,
},
}, },
disable: { );
method: 'PUT',
url: `${this.endpoint}{/id}/disable`,
},
});
} }
getKeys() { getKeys() {
return this.resource.get() return this.resource.get().then(response => response.json());
.then(response => response.json());
} }
enableKey(id) { enableKey(id) {
......
...@@ -3,7 +3,7 @@ export default class DeployKeysStore { ...@@ -3,7 +3,7 @@ export default class DeployKeysStore {
this.keys = {}; this.keys = {};
} }
findEnabledKey(id) { isEnabled(id) {
return this.keys.enabled_keys.find(key => key.id === id); return this.keys.enabled_keys.some(key => key.id === id);
} }
} }
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
<div class="environments-container"> <div class="environments-container">
<loading-icon <loading-icon
class="prepend-top-default"
label="Loading environments" label="Loading environments"
v-if="isLoading" v-if="isLoading"
size="3" size="3"
......
<script> <script>
import playIconSvg from 'icons/_icon_play.svg'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -8,9 +8,9 @@ ...@@ -8,9 +8,9 @@
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
loadingIcon, loadingIcon,
Icon,
}, },
props: { props: {
actions: { actions: {
...@@ -19,20 +19,16 @@ ...@@ -19,20 +19,16 @@
default: () => [], default: () => [],
}, },
}, },
data() { data() {
return { return {
playIconSvg,
isLoading: false, isLoading: false,
}; };
}, },
computed: { computed: {
title() { title() {
return 'Deploy to...'; return 'Deploy to...';
}, },
}, },
methods: { methods: {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
...@@ -65,7 +61,10 @@ ...@@ -65,7 +61,10 @@
:disabled="isLoading" :disabled="isLoading"
> >
<span> <span>
<span v-html="playIconSvg"></span> <icon
name="play"
:size="12"
/>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true" aria-hidden="true"
...@@ -86,7 +85,10 @@ ...@@ -86,7 +85,10 @@
:class="{ disabled: isActionDisabled(action) }" :class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)" :disabled="isActionDisabled(action)"
> >
<span v-html="playIconSvg"></span> <icon
name="play"
:size="12"
/>
<span> <span>
{{ action.name }} {{ action.name }}
</span> </span>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
...@@ -6,6 +7,9 @@ ...@@ -6,6 +7,9 @@
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
export default { export default {
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -15,7 +19,6 @@ ...@@ -15,7 +19,6 @@
required: true, required: true,
}, },
}, },
computed: { computed: {
title() { title() {
return s__('Environments|Open'); return s__('Environments|Open');
...@@ -34,10 +37,9 @@ ...@@ -34,10 +37,9 @@
:aria-label="title" :aria-label="title"
:href="externalUrl" :href="externalUrl"
> >
<i <icon
class="fa fa-external-link" name="external-link"
aria-hidden="true" :size="12"
> />
</i>
</a> </a>
</template> </template>
...@@ -2,20 +2,22 @@ ...@@ -2,20 +2,22 @@
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
monitoringUrl: { monitoringUrl: {
type: String, type: String,
required: true, required: true,
}, },
}, },
computed: { computed: {
title() { title() {
return 'Monitoring'; return 'Monitoring';
...@@ -33,10 +35,9 @@ ...@@ -33,10 +35,9 @@
:title="title" :title="title"
:aria-label="title" :aria-label="title"
> >
<i <icon
class="fa fa-area-chart" name="chart"
aria-hidden="true" :size="12"
> />
</i>
</a> </a>
</template> </template>
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
components: { components: {
loadingIcon, loadingIcon,
}, },
props: { props: {
retryUrl: { retryUrl: {
type: String, type: String,
...@@ -24,13 +23,11 @@ ...@@ -24,13 +23,11 @@
default: true, default: true,
}, },
}, },
data() { data() {
return { return {
isLoading: false, isLoading: false,
}; };
}, },
methods: { methods: {
onClick() { onClick() {
this.isLoading = true; this.isLoading = true;
......
...@@ -3,14 +3,16 @@ ...@@ -3,14 +3,16 @@
* Renders a terminal button to open a web terminal. * Renders a terminal button to open a web terminal.
* Used in environments table. * Used in environments table.
*/ */
import terminalIconSvg from 'icons/_icon_terminal.svg'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
terminalPath: { terminalPath: {
type: String, type: String,
...@@ -18,13 +20,6 @@ ...@@ -18,13 +20,6 @@
default: '', default: '',
}, },
}, },
data() {
return {
terminalIconSvg,
};
},
computed: { computed: {
title() { title() {
return 'Terminal'; return 'Terminal';
...@@ -40,7 +35,10 @@ ...@@ -40,7 +35,10 @@
:title="title" :title="title"
:aria-label="title" :aria-label="title"
:href="terminalPath" :href="terminalPath"
v-html="terminalIconSvg"
> >
<icon
name="terminal"
:size="12"
/>
</a> </a>
</template> </template>
...@@ -408,7 +408,10 @@ class GfmAutoComplete { ...@@ -408,7 +408,10 @@ class GfmAutoComplete {
fetchData($input, at) { fetchData($input, at) {
if (this.isLoadingData[at]) return; if (this.isLoadingData[at]) return;
this.isLoadingData[at] = true; this.isLoadingData[at] = true;
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
if (this.cachedData[at]) { if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
...@@ -418,12 +421,14 @@ class GfmAutoComplete { ...@@ -418,12 +421,14 @@ class GfmAutoComplete {
GfmAutoComplete.glEmojiTag = glEmojiTag; GfmAutoComplete.glEmojiTag = glEmojiTag;
}) })
.catch(() => { this.isLoadingData[at] = false; }); .catch(() => { this.isLoadingData[at] = false; });
} else { } else if (dataSource) {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) AjaxCache.retrieve(dataSource, true)
.then((data) => { .then((data) => {
this.loadData($input, at, data); this.loadData($input, at, data);
}) })
.catch(() => { this.isLoadingData[at] = false; }); .catch(() => { this.isLoadingData[at] = false; });
} else {
this.isLoadingData[at] = false;
} }
} }
......
...@@ -7,12 +7,12 @@ import { __ } from '~/locale'; ...@@ -7,12 +7,12 @@ import { __ } from '~/locale';
export default class GpgBadges { export default class GpgBadges {
static fetch() { static fetch() {
const badges = $('.js-loading-gpg-badge'); const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form'); const tag = $('.js-signature-container');
badges.html('<i class="fa fa-spinner fa-spin"></i>'); badges.html('<i class="fa fa-spinner fa-spin"></i>');
const params = parseQueryStringIntoObject(form.serialize()); const params = parseQueryStringIntoObject(tag.serialize());
return axios.get(form.data('signaturesPath'), { params }) return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => { .then(({ data }) => {
data.signatures.forEach((signature) => { data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { activityBarViews } from '../constants';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
computed: {
...mapGetters(['currentProject', 'hasChanges']),
...mapState(['currentActivityView']),
goBackUrl() {
return document.referrer || this.currentProject.web_url;
},
},
methods: {
...mapActions(['updateActivityBarView']),
},
activityBarViews,
};
</script>
<template>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li v-once>
<a
v-tooltip
data-container="body"
data-placement="right"
:href="goBackUrl"
class="ide-sidebar-link"
:title="s__('IDE|Go back')"
:aria-label="s__('IDE|Go back')"
>
<icon
:size="16"
name="go-back"
/>
</a>
</li>
<li>
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.edit
}"
@click.prevent="updateActivityBarView($options.activityBarViews.edit)"
:title="s__('IDE|Edit')"
:aria-label="s__('IDE|Edit')"
>
<icon
name="code"
/>
</button>
</li>
<li>
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-review-mode"
:class="{
active: currentActivityView === $options.activityBarViews.review
}"
@click.prevent="updateActivityBarView($options.activityBarViews.review)"
:title="s__('IDE|Review')"
:aria-label="s__('IDE|Review')"
>
<icon
name="file-modified"
/>
</button>
</li>
<li v-show="hasChanges">
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-commit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.commit
}"
@click.prevent="updateActivityBarView($options.activityBarViews.commit)"
:title="s__('IDE|Commit')"
:aria-label="s__('IDE|Commit')"
>
<icon
name="commit"
/>
</button>
</li>
</ul>
</nav>
</template>
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
return `${this.changedIcon}-solid`; return `${this.changedIcon}-solid`;
}, },
changedIconClass() { changedIconClass() {
return `multi-${this.changedIcon} prepend-left-5 pull-left`; return `multi-${this.changedIcon} pull-left`;
}, },
tooltipTitle() { tooltipTitle() {
if (!this.showTooltip) return undefined; if (!this.showTooltip) return undefined;
...@@ -79,13 +79,7 @@ export default { ...@@ -79,13 +79,7 @@ export default {
class="ide-file-changed-icon" class="ide-file-changed-icon"
> >
<icon <icon
v-if="file.staged && showStagedIcon" v-if="file.changed || file.tempFile || file.staged"
:name="stagedIcon"
:size="12"
:css-classes="changedIconClass"
/>
<icon
v-if="file.changed || file.tempFile || (file.staged && !showStagedIcon)"
:name="changedIcon" :name="changedIcon"
:size="12" :size="12"
:css-classes="changedIconClass" :css-classes="changedIconClass"
......
<script> <script>
import { mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants'; import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue'; import RadioGroup from './radio_group.vue';
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
RadioGroup, RadioGroup,
}, },
computed: { computed: {
...mapState(['currentBranchId']), ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
commitToCurrentBranchText() { commitToCurrentBranchText() {
return sprintf( return sprintf(
__('Commit to %{branchName} branch'), __('Commit to %{branchName} branch'),
...@@ -17,6 +17,17 @@ export default { ...@@ -17,6 +17,17 @@ export default {
false, false,
); );
}, },
disableMergeRequestRadio() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
},
},
mounted() {
if (this.disableMergeRequestRadio) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
}
},
methods: {
...mapActions('commit', ['updateCommitAction']),
}, },
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
...@@ -44,6 +55,7 @@ export default { ...@@ -44,6 +55,7 @@ export default {
:value="$options.commitToNewBranchMR" :value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')" :label="__('Create a new branch and merge request')"
:show-input="true" :show-input="true"
:disabled="disableMergeRequestRadio"
/> />
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState(['lastCommitMsg', 'rightPanelCollapsed', 'changedFiles', 'stagedFiles']), ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
},
methods: {
...mapActions(['toggleRightPanelCollapsed']),
}, },
}; };
</script> </script>
...@@ -31,31 +13,8 @@ export default { ...@@ -31,31 +13,8 @@ export default {
v-if="!lastCommitMsg" v-if="!lastCommitMsg"
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state" class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
> >
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<button
v-tooltip
:title="collapseButtonTooltip"
data-container="body"
data-placement="left"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
:aria-label="__('Toggle sidebar')"
@click.stop="toggleRightPanelCollapsed"
>
<icon
:name="collapseButtonIcon"
:size="18"
/>
</button>
</header>
<div <div
class="ide-commit-empty-state-container" class="ide-commit-empty-state-container"
v-if="!rightPanelCollapsed"
> >
<div class="svg-content svg-80"> <div class="svg-content svg-80">
<img :src="noChangesStateSvgPath" /> <img :src="noChangesStateSvgPath" />
......
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { sprintf, __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
export default {
components: {
Actions,
LoadingButton,
CommitMessageField,
SuccessMessage,
},
data() {
return {
isCompact: true,
componentHeight: null,
};
},
computed: {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['hasChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
overviewText() {
return sprintf(
__(
'<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
),
{
stagedFilesLength: this.stagedFiles.length,
changedFilesLength: this.changedFiles.length,
},
);
},
},
watch: {
currentActivityView() {
if (this.lastCommitMsg) {
this.isCompact = false;
} else {
this.isCompact = !(
this.currentActivityView === activityBarViews.commit &&
window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
);
}
},
lastCommitMsg() {
this.isCompact =
this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
},
},
methods: {
...mapActions(['updateActivityBarView']),
...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
toggleIsSmall() {
this.updateActivityBarView(activityBarViews.commit)
.then(() => {
this.isCompact = !this.isCompact;
})
.catch(e => {
throw e;
});
},
beforeEnterTransition() {
const elHeight = this.isCompact
? this.$refs.formEl && this.$refs.formEl.offsetHeight
: this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
},
enterTransition() {
this.$nextTick(() => {
const elHeight = this.isCompact
? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
: this.$refs.formEl && this.$refs.formEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
});
},
afterEndTransition() {
this.componentHeight = null;
},
},
activityBarViews,
};
</script>
<template>
<div
class="multi-file-commit-form"
:class="{
'is-compact': isCompact,
'is-full': !isCompact
}"
:style="{
height: componentHeight ? `${componentHeight}px` : null,
}"
>
<transition
name="commit-form-slide-up"
@before-enter="beforeEnterTransition"
@enter="enterTransition"
@after-enter="afterEndTransition"
>
<div
v-if="isCompact"
class="commit-form-compact"
ref="compactEl"
>
<button
type="button"
:disabled="!hasChanges"
class="btn btn-primary btn-sm btn-block"
@click="toggleIsSmall"
>
{{ __('Commit') }}
</button>
<p
class="text-center"
v-html="overviewText"
></p>
</div>
<form
v-if="!isCompact"
class="form-horizontal"
@submit.prevent.stop="commitChanges"
ref="formEl"
>
<transition name="fade">
<success-message
v-show="lastCommitMsg"
/>
</transition>
<commit-message-field
:text="commitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
class="btn btn-default btn-sm pull-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
</button>
<button
v-else
type="button"
class="btn btn-default btn-sm pull-right"
@click="toggleIsSmall"
>
{{ __('Collapse') }}
</button>
</div>
</form>
</transition>
</div>
</template>
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue'; import ListItem from './list_item.vue';
import ListCollapsed from './list_collapsed.vue';
export default { export default {
components: { components: {
Icon, Icon,
ListItem, ListItem,
ListCollapsed,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -24,11 +22,6 @@ export default { ...@@ -24,11 +22,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
showToggle: {
type: Boolean,
required: false,
default: true,
},
iconName: { iconName: {
type: String, type: String,
required: true, required: true,
...@@ -51,9 +44,12 @@ export default { ...@@ -51,9 +44,12 @@ export default {
default: false, default: false,
}, },
}, },
data() {
return {
showActionButton: false,
};
},
computed: { computed: {
...mapState(['rightPanelCollapsed']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
titleText() { titleText() {
return sprintf(__('%{title} changes'), { return sprintf(__('%{title} changes'), {
title: this.title, title: this.title,
...@@ -61,10 +57,13 @@ export default { ...@@ -61,10 +57,13 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']), ...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() { actionBtnClicked() {
this[this.action](); this[this.action]();
}, },
setShowActionButton(show) {
this.showActionButton = show;
},
}, },
}; };
</script> </script>
...@@ -72,19 +71,14 @@ export default { ...@@ -72,19 +71,14 @@ export default {
<template> <template>
<div <div
class="ide-commit-list-container" class="ide-commit-list-container"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
> >
<header <header
class="multi-file-commit-panel-header" class="multi-file-commit-panel-header"
@mouseenter="setShowActionButton(true)"
@mouseleave="setShowActionButton(false)"
> >
<div <div
v-if="!rightPanelCollapsed"
class="multi-file-commit-panel-header-title" class="multi-file-commit-panel-header-title"
:class="{
'append-right-10': showToggle,
}"
> >
<icon <icon
v-once v-once
...@@ -92,7 +86,14 @@ export default { ...@@ -92,7 +86,14 @@ export default {
:size="18" :size="18"
/> />
{{ titleText }} {{ titleText }}
<span
v-show="!showActionButton"
class="ide-commit-file-count"
>
{{ fileList.length }}
</span>
<button <button
v-show="showActionButton"
type="button" type="button"
class="btn btn-blank btn-link ide-staged-action-btn" class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked" @click="actionBtnClicked"
...@@ -100,52 +101,28 @@ export default { ...@@ -100,52 +101,28 @@ export default {
{{ actionBtnText }} {{ actionBtnText }}
</button> </button>
</div> </div>
<button
v-if="showToggle"
v-tooltip
:title="collapseButtonTooltip"
data-container="body"
data-placement="left"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
:aria-label="__('Toggle sidebar')"
@click.stop="toggleRightPanelCollapsed"
>
<icon
:name="collapseButtonIcon"
:size="18"
/>
</button>
</header> </header>
<list-collapsed <ul
v-if="rightPanelCollapsed" v-if="fileList.length"
:files="fileList" class="multi-file-commit-list list-unstyled append-bottom-0"
:icon-name="iconName" >
:title="title" <li
/> v-for="file in fileList"
<template v-else> :key="file.key"
<ul
v-if="fileList.length"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
<list-item
:file="file"
:action-component="itemActionComponent"
:key-prefix="title"
:staged-list="stagedList"
/>
</li>
</ul>
<p
v-else
class="multi-file-commit-list help-block"
> >
{{ __('No changes') }} <list-item
</p> :file="file"
</template> :action-component="itemActionComponent"
:key-prefix="title"
:staged-list="stagedList"
/>
</li>
</ul>
<p
v-else
class="multi-file-commit-list help-block"
>
{{ __('No changes') }}
</p>
</div> </div>
</template> </template>
...@@ -3,6 +3,7 @@ import { mapActions } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue'; import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue'; import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
export default { export default {
components: { components: {
...@@ -36,7 +37,7 @@ export default { ...@@ -36,7 +37,7 @@ export default {
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`; return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
}, },
iconClass() { iconClass() {
return `multi-file-${this.file.tempFile ? 'additions' : 'modified'} append-right-8`; return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
}, },
}, },
methods: { methods: {
...@@ -53,7 +54,7 @@ export default { ...@@ -53,7 +54,7 @@ export default {
keyPrefix: this.keyPrefix.toLowerCase(), keyPrefix: this.keyPrefix.toLowerCase(),
}).then(changeViewer => { }).then(changeViewer => {
if (changeViewer) { if (changeViewer) {
this.updateViewer('diff'); this.updateViewer(viewerTypes.diff);
} }
}); });
}, },
......
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
...@@ -26,10 +27,20 @@ export default { ...@@ -26,10 +27,20 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
disabled: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...mapState('commit', ['commitAction']), ...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']), ...mapGetters('commit', ['newBranchName']),
tooltipTitle() {
return this.disabled
? __('This option is disabled while you still have unstaged changes')
: '';
},
}, },
methods: { methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']), ...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
...@@ -39,19 +50,28 @@ export default { ...@@ -39,19 +50,28 @@ export default {
<template> <template>
<fieldset> <fieldset>
<label> <label
v-tooltip
:title="tooltipTitle"
:class="{
'is-disabled': disabled
}"
>
<input <input
type="radio" type="radio"
name="commit-action" name="commit-action"
:value="value" :value="value"
@change="updateCommitAction($event.target.value)" @change="updateCommitAction($event.target.value)"
:checked="checked" :checked="commitAction === value"
v-once :disabled="disabled"
/> />
<span class="prepend-left-10"> <span class="prepend-left-10">
<template v-if="label"> <span
v-if="label"
class="ide-radio-label"
>
{{ label }} {{ label }}
</template> </span>
<slot v-else></slot> <slot v-else></slot>
</span> </span>
</label> </label>
......
...@@ -2,14 +2,8 @@ ...@@ -2,14 +2,8 @@
import { mapState } from 'vuex'; import { mapState } from 'vuex';
export default { export default {
props: {
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState(['lastCommitMsg']), ...mapState(['lastCommitMsg', 'committedStateSvgPath']),
}, },
}; };
</script> </script>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { viewerTypes } from '../constants';
export default { export default {
components: {
Icon,
},
props: { props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
mergeRequestId: {
type: String,
required: false,
default: '',
},
viewer: { viewer: {
type: String, type: String,
required: true, required: true,
}, },
showShadow: { mergeRequestId: {
type: Boolean, type: Number,
required: true, required: true,
}, },
}, },
...@@ -38,84 +25,45 @@ export default { ...@@ -38,84 +25,45 @@ export default {
this.$emit('click', mode); this.$emit('click', mode);
}, },
}, },
viewerTypes,
}; };
</script> </script>
<template> <template>
<div <div
class="dropdown" class="dropdown"
:class="{
shadow: showShadow,
}"
> >
<button <button
type="button" type="button"
class="btn btn-primary btn-sm" class="btn btn-link"
:class="{
'btn-inverted': hasChanges,
}"
data-toggle="dropdown" data-toggle="dropdown"
> >
<template v-if="viewer === 'mrdiff' && mergeRequestId"> {{ __('Edit') }}
{{ mergeReviewLine }}
</template>
<template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
{{ __('Reviewing') }}
</template>
<icon
name="angle-down"
:size="12"
css-classes="caret-down"
/>
</button> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left"> <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul> <ul>
<template v-if="mergeRequestId">
<li>
<a
href="#"
@click.prevent="changeMode('mrdiff')"
:class="{
'is-active': viewer === 'mrdiff',
}"
>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li
role="separator"
class="divider"
>
</li>
</template>
<li> <li>
<a <a
href="#" href="#"
@click.prevent="changeMode('editor')" @click.prevent="changeMode($options.viewerTypes.mr)"
:class="{ :class="{
'is-active': viewer === 'editor', 'is-active': viewer === $options.viewerTypes.mr,
}" }"
> >
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong> <strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content"> <span class="dropdown-menu-inner-content">
{{ __('View and edit lines') }} {{ __('Compare changes with the merge request target branch') }}
</span> </span>
</a> </a>
</li> </li>
<li> <li>
<a <a
href="#" href="#"
@click.prevent="changeMode('diff')" @click.prevent="changeMode($options.viewerTypes.diff)"
:class="{ :class="{
'is-active': viewer === 'diff', 'is-active': viewer === $options.viewerTypes.diff,
}" }"
> >
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong> <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
......
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import Mousetrap from 'mousetrap';
import Mousetrap from 'mousetrap'; import { mapActions, mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue'; import IdeSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue'; import RepoTabs from './repo_tabs.vue';
import repoTabs from './repo_tabs.vue'; import IdeStatusBar from './ide_status_bar.vue';
import ideStatusBar from './ide_status_bar.vue'; import RepoEditor from './repo_editor.vue';
import repoEditor from './repo_editor.vue'; import FindFile from './file_finder/index.vue';
import FindFile from './file_finder/index.vue';
const originalStopCallback = Mousetrap.stopCallback; const originalStopCallback = Mousetrap.stopCallback;
export default { export default {
components: { components: {
ideSidebar, IdeSidebar,
ideContextbar, RepoTabs,
repoTabs, IdeStatusBar,
ideStatusBar, RepoEditor,
repoEditor, FindFile,
FindFile, },
}, computed: {
props: { ...mapState([
emptyStateSvgPath: { 'changedFiles',
type: String, 'openFiles',
required: true, 'viewer',
}, 'currentMergeRequestId',
noChangesStateSvgPath: { 'fileFindVisible',
type: String, 'emptyStateSvgPath',
required: true, ]),
}, ...mapGetters(['activeFile', 'hasChanges']),
committedStateSvgPath: { },
type: String, mounted() {
required: true, const returnValue = 'Are you sure you want to lose unsaved changes?';
}, window.onbeforeunload = e => {
}, if (!this.changedFiles.length) return undefined;
computed: {
...mapState([
'changedFiles',
'openFiles',
'viewer',
'currentMergeRequestId',
'fileFindVisible',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, { Object.assign(e, {
returnValue, returnValue,
}); });
return returnValue; return returnValue;
}; };
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => { Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) { if (e.preventDefault) {
e.preventDefault(); e.preventDefault();
} }
this.toggleFileFinder(!this.fileFindVisible); this.toggleFileFinder(!this.fileFindVisible);
}); });
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo); Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
}, },
methods: { methods: {
...mapActions(['toggleFileFinder']), ...mapActions(['toggleFileFinder']),
mousetrapStopCallback(e, el, combo) { mousetrapStopCallback(e, el, combo) {
if (combo === 't' && el.classList.contains('dropdown-input-field')) { if (combo === 't' && el.classList.contains('dropdown-input-field')) {
return true; return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') { } else if (combo === 'command+p' || combo === 'ctrl+p') {
return false; return false;
} }
return originalStopCallback(e, el, combo); return originalStopCallback(e, el, combo);
},
}, },
}; },
};
</script> </script>
<template> <template>
<div <article class="ide">
class="ide-view"
>
<find-file
v-show="fileFindVisible"
/>
<ide-sidebar />
<div <div
class="multi-file-edit-pane" class="ide-view"
> >
<template <find-file
v-if="activeFile" v-show="fileFindVisible"
> />
<repo-tabs <ide-sidebar />
:active-file="activeFile" <div
:files="openFiles" class="multi-file-edit-pane"
:viewer="viewer"
:has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
:file="activeFile"
/>
<ide-status-bar
:file="activeFile"
/>
</template>
<template
v-else
> >
<div <template
v-once v-if="activeFile"
class="ide-empty-state"
> >
<div class="row js-empty-state"> <repo-tabs
<div class="col-xs-12"> :active-file="activeFile"
<div class="svg-content svg-250"> :files="openFiles"
<img :src="emptyStateSvgPath" /> :viewer="viewer"
:has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
:file="activeFile"
/>
</template>
<template
v-else
>
<div
v-once
class="ide-empty-state"
>
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
</div>
</div> </div>
</div> <div class="col-xs-12">
<div class="col-xs-12"> <div class="text-content text-center">
<div class="text-content text-center"> <h4>
<h4> Welcome to the GitLab IDE
Welcome to the GitLab IDE </h4>
</h4> <p>
<p> You can select a file in the left sidebar to begin
You can select a file in the left sidebar to begin editing and use the right sidebar to commit your changes.
editing and use the right sidebar to commit your changes. </p>
</p> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</template> </div>
</div> </div>
<ide-contextbar <ide-status-bar
:no-changes-state-svg-path="noChangesStateSvgPath" :file="activeFile"
:committed-state-svg-path="committedStateSvgPath"
/> />
</div> </article>
</template> </template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
import ResizablePanel from './resizable_panel.vue';
export default {
components: {
repoCommitSection,
icon,
panelResizer,
ResizablePanel,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<resizable-panel
:collapsible="true"
:initial-width="340"
side="right"
>
<div
class="multi-file-commit-panel-section"
>
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
</resizable-panel>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
projectUrl: {
type: String,
required: true,
},
},
computed: {
goBackUrl() {
return document.referrer || this.projectUrl;
},
},
};
</script>
<template>
<nav
class="ide-external-links"
v-once
>
<p>
<a
:href="goBackUrl"
class="ide-sidebar-link"
>
<icon
:size="16"
class="append-right-8"
name="go-back"
/>
<span class="ide-external-links-text">
{{ s__('Go back') }}
</span>
</a>
</p>
</nav>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue';
import newDropdown from './new_dropdown/index.vue';
export default {
components: {
repoTree,
icon,
newDropdown,
},
props: {
projectId: {
type: String,
required: true,
},
branch: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="branch-container">
<div class="branch-header">
<div class="branch-header-title str-truncated ref-name">
<icon
name="branch"
:size="12"
/>
{{ branch.name }}
</div>
<div class="branch-header-btns">
<new-dropdown
:project-id="projectId"
:branch="branch.name"
path=""
/>
</div>
</div>
<repo-tree
:tree="branch.tree"
/>
</div>
</template>
<script>
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Identicon from '../../vue_shared/components/identicon.vue';
import BranchesTree from './ide_project_branches_tree.vue';
import ExternalLinks from './ide_external_links.vue';
export default {
components: {
BranchesTree,
ExternalLinks,
ProjectAvatarImage,
Identicon,
},
props: {
project: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="projects-sidebar">
<div class="context-header">
<a
:title="project.name"
:href="project.web_url"
>
<div
v-if="project.avatar_url"
class="avatar-container s40 project-avatar"
>
<project-avatar-image
class="avatar-container project-avatar"
:link-href="project.path"
:img-src="project.avatar_url"
:img-alt="project.name"
:img-size="40"
/>
</div>
<identicon
v-else
size-class="s40"
:entity-id="project.id"
:entity-name="project.name"
/>
<div class="sidebar-context-title">
{{ project.name }}
</div>
</a>
</div>
<external-links
:project-url="project.web_url"
/>
<div class="multi-file-commit-panel-inner-scroll">
<branches-tree
v-for="branch in project.branches"
:key="branch.name"
:project-id="project.path_with_namespace"
:branch="branch"
/>
</div>
</div>
</template>
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import IdeTreeList from './ide_tree_list.vue';
import EditorModeDropdown from './editor_mode_dropdown.vue';
import { viewerTypes } from '../constants';
export default {
components: {
IdeTreeList,
EditorModeDropdown,
},
computed: {
...mapGetters(['currentMergeRequest']),
...mapState(['viewer']),
showLatestChangesText() {
return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
},
showMergeRequestText() {
return this.currentMergeRequest && this.viewer === viewerTypes.mr;
},
},
mounted() {
this.$nextTick(() => {
this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
});
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
<ide-tree-list
:viewer-type="viewer"
header-class="ide-review-header"
:disable-action-dropdown="true"
>
<template
slot="header"
>
<div class="ide-review-button-holder">
{{ __('Review') }}
<editor-mode-dropdown
v-if="currentMergeRequest"
:viewer="viewer"
:merge-request-id="currentMergeRequest.iid"
@click="updateViewer"
/>
</div>
<div class="prepend-top-5 ide-review-sub-header">
<template v-if="showLatestChangesText">
{{ __('Latest changes') }}
</template>
<template v-else-if="showMergeRequestText">
{{ __('Merge request') }}
(<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
</template>
</div>
</template>
</ide-tree-list>
</template>
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue'; import Icon from '~/vue_shared/components/icon.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import tooltip from '~/vue_shared/directives/tooltip';
import projectTree from './ide_project_tree.vue'; import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import ResizablePanel from './resizable_panel.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import Identicon from '../../vue_shared/components/identicon.vue';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
import CommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeReview from './ide_review.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
import { activityBarViews } from '../constants';
export default { export default {
components: { directives: {
projectTree, tooltip,
icon, },
panelResizer, components: {
skeletonLoadingContainer, Icon,
ResizablePanel, PanelResizer,
SkeletonLoadingContainer,
ResizablePanel,
ActivityBar,
ProjectAvatarImage,
Identicon,
CommitSection,
IdeTree,
CommitForm,
IdeReview,
SuccessMessage,
},
data() {
return {
showTooltip: false,
};
},
computed: {
...mapState([
'loading',
'currentBranchId',
'currentActivityView',
'changedFiles',
'stagedFiles',
'lastCommitMsg',
]),
...mapGetters(['currentProject', 'someUncommitedChanges']),
showSuccessMessage() {
return (
this.currentActivityView === activityBarViews.edit &&
(this.lastCommitMsg && !this.someUncommitedChanges)
);
}, },
computed: { branchTooltipTitle() {
...mapState([ return this.showTooltip ? this.currentBranchId : undefined;
'loading',
]),
...mapGetters([
'projectsWithTrees',
]),
}, },
}; },
watch: {
currentBranchId() {
this.$nextTick(() => {
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
});
},
},
};
</script> </script>
<template> <template>
<resizable-panel <resizable-panel
:collapsible="false" :collapsible="false"
:initial-width="290" :initial-width="340"
side="left" side="left"
> >
<activity-bar
v-if="!loading"
/>
<div class="multi-file-commit-panel-inner"> <div class="multi-file-commit-panel-inner">
<template v-if="loading"> <template v-if="loading">
<div <div
...@@ -41,11 +87,54 @@ ...@@ -41,11 +87,54 @@
<skeleton-loading-container /> <skeleton-loading-container />
</div> </div>
</template> </template>
<project-tree <template v-else>
v-for="project in projectsWithTrees" <div class="context-header ide-context-header">
:key="project.id" <a
:project="project" :href="currentProject.web_url"
/> >
<div
v-if="currentProject.avatar_url"
class="avatar-container s40 project-avatar"
>
<project-avatar-image
class="avatar-container project-avatar"
:link-href="currentProject.path"
:img-src="currentProject.avatar_url"
:img-alt="currentProject.name"
:img-size="40"
/>
</div>
<identicon
v-else
size-class="s40"
:entity-id="currentProject.id"
:entity-name="currentProject.name"
/>
<div class="ide-sidebar-project-title">
<div class="sidebar-context-title">
{{ currentProject.name }}
</div>
<div
class="sidebar-context-title ide-sidebar-branch-title"
ref="branchId"
v-tooltip
:title="branchTooltipTitle"
>
<icon
name="branch"
css-classes="append-right-5"
/>{{ currentBranchId }}
</div>
</div>
</a>
</div>
<div class="multi-file-commit-panel-inner-scroll">
<component
:is="currentActivityView"
/>
</div>
<commit-form />
</template>
</div> </div>
</resizable-panel> </resizable-panel>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default { export default {
components: { components: {
icon, icon,
userAvatarImage,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -14,40 +17,93 @@ export default { ...@@ -14,40 +17,93 @@ export default {
props: { props: {
file: { file: {
type: Object, type: Object,
required: true, required: false,
default: null,
},
},
data() {
return {
lastCommitFormatedAge: null,
};
},
computed: {
...mapGetters(['currentProject', 'lastCommit']),
},
mounted() {
this.startTimer();
},
beforeDestroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
},
methods: {
startTimer() {
this.intervalId = setInterval(() => {
this.commitAgeUpdate();
}, 1000);
},
commitAgeUpdate() {
if (this.lastCommit) {
this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
}
},
getCommitPath(shortSha) {
return `${this.currentProject.web_url}/commit/${shortSha}`;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="ide-status-bar"> <footer class="ide-status-bar">
<div> <div
<div v-if="file.lastCommit && file.lastCommit.id"> class="ide-status-branch"
Last commit: v-if="lastCommit && lastCommitFormatedAge"
<a >
v-tooltip <icon
:title="file.lastCommit.message" name="commit"
:href="file.lastCommit.url" />
> <a
{{ timeFormated(file.lastCommit.updatedAt) }} by v-tooltip
{{ file.lastCommit.author }} class="commit-sha"
</a> :title="lastCommit.message"
</div> :href="getCommitPath(lastCommit.short_id)"
>{{ lastCommit.short_id }}</a>
by
{{ lastCommit.author_name }}
<time
v-tooltip
data-placement="top"
data-container="body"
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
>
{{ lastCommitFormatedAge }}
</time>
</div> </div>
<div class="text-right"> <div
v-if="file"
class="ide-status-file"
>
{{ file.name }} {{ file.name }}
</div> </div>
<div class="text-right"> <div
v-if="file"
class="ide-status-file"
>
{{ file.eol }} {{ file.eol }}
</div> </div>
<div <div
class="text-right" class="ide-status-file"
v-if="!file.binary"> v-if="file && !file.binary">
{{ file.editorRow }}:{{ file.editorColumn }} {{ file.editorRow }}:{{ file.editorColumn }}
</div> </div>
<div class="text-right"> <div
v-if="file"
class="ide-status-file"
>
{{ file.fileLanguage }} {{ file.fileLanguage }}
</div> </div>
</div> </footer>
</template> </template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import NewDropdown from './new_dropdown/index.vue';
import IdeTreeList from './ide_tree_list.vue';
export default {
components: {
NewDropdown,
IdeTreeList,
},
computed: {
...mapState(['currentBranchId']),
...mapGetters(['currentProject', 'currentTree', 'activeFile']),
},
mounted() {
if (this.activeFile && this.activeFile.pending) {
this.$router.push(`/project${this.activeFile.url}`, () => {
this.updateViewer('editor');
});
}
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
<ide-tree-list
viewer-type="editor"
>
<template
slot="header"
>
{{ __('Edit') }}
<new-dropdown
:project-id="currentProject.name_with_namespace"
:branch="currentBranchId"
/>
</template>
</ide-tree-list>
</template>
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue'; import RepoFile from './repo_file.vue';
import NewDropdown from './new_dropdown/index.vue';
export default { export default {
components: { components: {
Icon,
RepoFile, RepoFile,
SkeletonLoadingContainer, SkeletonLoadingContainer,
NewDropdown,
}, },
props: { props: {
tree: { viewerType: {
type: Object, type: String,
required: true, required: true,
}, },
headerClass: {
type: String,
required: false,
default: null,
},
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['currentBranchId']),
...mapGetters(['currentProject', 'currentTree']),
showLoading() {
return !this.currentTree || this.currentTree.loading;
},
},
mounted() {
this.updateViewer(this.viewerType);
},
methods: {
...mapActions(['updateViewer']),
}, },
}; };
</script> </script>
...@@ -20,7 +48,7 @@ export default { ...@@ -20,7 +48,7 @@ export default {
<div <div
class="ide-file-list" class="ide-file-list"
> >
<template v-if="tree.loading"> <template v-if="showLoading">
<div <div
class="multi-file-loading-container" class="multi-file-loading-container"
v-for="n in 3" v-for="n in 3"
...@@ -30,11 +58,18 @@ export default { ...@@ -30,11 +58,18 @@ export default {
</div> </div>
</template> </template>
<template v-else> <template v-else>
<header
class="ide-tree-header"
:class="headerClass"
>
<slot name="header"></slot>
</header>
<repo-file <repo-file
v-for="file in tree.tree" v-for="file in currentTree.tree"
:key="file.key" :key="file.key"
:file="file" :file="file"
:level="0" :level="0"
:disable-action-dropdown="disableActionDropdown"
/> />
</template> </template>
</div> </div>
......
...@@ -17,7 +17,8 @@ export default { ...@@ -17,7 +17,8 @@ export default {
}, },
path: { path: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
}, },
data() { data() {
......
...@@ -3,13 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex'; ...@@ -3,13 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitFilesList from './commit_sidebar/list.vue'; import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue'; import EmptyState from './commit_sidebar/empty_state.vue';
import CommitMessageField from './commit_sidebar/message_field.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
import * as consts from '../stores/modules/commit/constants'; import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue'; import { activityBarViews } from '../constants';
export default { export default {
components: { components: {
...@@ -17,42 +14,50 @@ export default { ...@@ -17,42 +14,50 @@ export default {
Icon, Icon,
CommitFilesList, CommitFilesList,
EmptyState, EmptyState,
SuccessMessage,
Actions,
LoadingButton,
CommitMessageField,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState([
'changedFiles',
'stagedFiles',
'rightPanelCollapsed',
'lastCommitMsg',
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
showStageUnstageArea() { showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
}, },
someUncommitedChanges() { },
return !!(this.changedFiles.length || this.stagedFiles.length); watch: {
hasChanges() {
if (!this.hasChanges) {
this.updateActivityBarView(activityBarViews.edit);
}
}, },
...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed', 'lastCommitMsg', 'unusedSeal']), },
...mapState('commit', ['commitMessage', 'submitCommitLoading']), mounted() {
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), if (this.lastOpenedFile) {
this.openPendingTab({
file: this.lastOpenedFile,
})
.then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
})
.catch(e => {
throw e;
});
}
}, },
methods: { methods: {
...mapActions('commit', [ ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
'updateCommitMessage', ...mapActions('commit', ['commitChanges', 'updateCommitAction']),
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
forceCreateNewBranch() { forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges()); return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
}, },
...@@ -80,6 +85,7 @@ export default { ...@@ -80,6 +85,7 @@ export default {
v-if="showStageUnstageArea" v-if="showStageUnstageArea"
> >
<commit-files-list <commit-files-list
class="is-first"
icon-name="unstaged" icon-name="unstaged"
:title="__('Unstaged')" :title="__('Unstaged')"
:file-list="changedFiles" :file-list="changedFiles"
...@@ -94,49 +100,11 @@ export default { ...@@ -94,49 +100,11 @@ export default {
action="unstageAllChanges" action="unstageAllChanges"
:action-btn-text="__('Unstage all')" :action-btn-text="__('Unstage all')"
item-action-component="unstage-button" item-action-component="unstage-button"
:show-toggle="false"
:staged-list="true" :staged-list="true"
/> />
</template> </template>
<empty-state <empty-state
v-if="unusedSeal" v-if="unusedSeal"
:no-changes-state-svg-path="noChangesStateSvgPath"
/> />
<div
class="multi-file-commit-panel-bottom"
>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
<success-message
v-if="lastCommitMsg && !someUncommitedChanges"
:committed-state-svg-path="committedStateSvgPath"
/>
<commit-message-field
:text="commitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
class="btn btn-default btn-sm pull-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
</button>
</div>
</form>
</div>
</div> </div>
</template> </template>
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash'; import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue'; import IdeFileButtons from './ide_file_buttons.vue';
...@@ -19,8 +20,14 @@ export default { ...@@ -19,8 +20,14 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']), ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
...mapGetters(['currentMergeRequest', 'getStagedFile']), ...mapGetters([
'currentMergeRequest',
'getStagedFile',
'isEditModeActive',
'isCommitModeActive',
'isReviewModeActive',
]),
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.content; return this.file && this.file.binary && !this.file.content;
}, },
...@@ -40,6 +47,21 @@ export default { ...@@ -40,6 +47,21 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently // Compare key to allow for files opened in review mode to be cached differently
if (newVal.key !== this.file.key) { if (newVal.key !== this.file.key) {
this.initMonaco(); this.initMonaco();
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
viewMode: 'edit',
});
}
}
},
currentActivityView() {
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
viewMode: 'edit',
});
} }
}, },
rightPanelCollapsed() { rightPanelCollapsed() {
...@@ -77,7 +99,6 @@ export default { ...@@ -77,7 +99,6 @@ export default {
'setFileViewMode', 'setFileViewMode',
'setFileEOL', 'setFileEOL',
'updateViewer', 'updateViewer',
'updateDelayViewerUpdated',
]), ]),
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
...@@ -89,14 +110,6 @@ export default { ...@@ -89,14 +110,6 @@ export default {
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
}) })
.then(() => { .then(() => {
const viewerPromise = this.delayViewerUpdated
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
: Promise.resolve();
return viewerPromise;
})
.then(() => {
this.updateDelayViewerUpdated(false);
this.createEditorInstance(); this.createEditorInstance();
}) })
.catch(err => { .catch(err => {
...@@ -108,10 +121,10 @@ export default { ...@@ -108,10 +121,10 @@ export default {
this.editor.dispose(); this.editor.dispose();
this.$nextTick(() => { this.$nextTick(() => {
if (this.viewer === 'editor') { if (this.viewer === viewerTypes.edit) {
this.editor.createInstance(this.$refs.editor); this.editor.createInstance(this.$refs.editor);
} else { } else {
this.editor.createDiffInstance(this.$refs.editor); this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
} }
this.setupEditor(); this.setupEditor();
...@@ -127,7 +140,7 @@ export default { ...@@ -127,7 +140,7 @@ export default {
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null, this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
); );
if (this.viewer === 'mrdiff') { if (this.viewer === viewerTypes.mr) {
this.editor.attachMergeRequestModel(this.model); this.editor.attachMergeRequestModel(this.model);
} else { } else {
this.editor.attachModel(this.model); this.editor.attachModel(this.model);
...@@ -168,6 +181,7 @@ export default { ...@@ -168,6 +181,7 @@ export default {
}); });
}, },
}, },
viewerTypes,
}; };
</script> </script>
...@@ -176,16 +190,17 @@ export default { ...@@ -176,16 +190,17 @@ export default {
id="ide" id="ide"
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div class="ide-mode-tabs clearfix"> <div class="ide-mode-tabs clearfix" >
<ul <ul
class="nav-links pull-left" class="nav-links pull-left"
v-if="!shouldHideEditor"> v-if="!shouldHideEditor && isEditModeActive"
>
<li :class="editTabCSS"> <li :class="editTabCSS">
<a <a
href="javascript:void(0);" href="javascript:void(0);"
role="button" role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })"> @click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
<template v-if="viewer === 'editor'"> <template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }} {{ __('Edit') }}
</template> </template>
<template v-else> <template v-else>
...@@ -212,6 +227,9 @@ export default { ...@@ -212,6 +227,9 @@ export default {
v-show="!shouldHideEditor && file.viewMode === 'edit'" v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor" ref="editor"
class="multi-file-editor-holder" class="multi-file-editor-holder"
:class="{
'is-readonly': isCommitModeActive,
}"
> >
</div> </div>
<content-viewer <content-viewer
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import { n__, __, sprintf } from '~/locale';
import fileIcon from '~/vue_shared/components/file_icon.vue'; import tooltip from '~/vue_shared/directives/tooltip';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router'; import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue'; import NewDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue'; import FileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue'; import ChangedFileIcon from './changed_file_icon.vue';
import mrFileIcon from './mr_file_icon.vue'; import MrFileIcon from './mr_file_icon.vue';
export default { export default {
name: 'RepoFile', name: 'RepoFile',
directives: {
tooltip,
},
components: { components: {
skeletonLoadingContainer, SkeletonLoadingContainer,
newDropdown, NewDropdown,
fileStatusIcon, FileStatusIcon,
fileIcon, FileIcon,
changedFileIcon, ChangedFileIcon,
mrFileIcon, MrFileIcon,
Icon,
}, },
props: { props: {
file: { file: {
...@@ -27,8 +34,41 @@ export default { ...@@ -27,8 +34,41 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...mapGetters([
'getChangesInFolder',
'getUnstagedFilesCountForPath',
'getStagedFilesCountForPath',
]),
folderUnstagedCount() {
return this.getUnstagedFilesCountForPath(this.file.path);
},
folderStagedCount() {
return this.getStagedFilesCountForPath(this.file.path);
},
changesCount() {
return this.getChangesInFolder(this.file.path);
},
folderChangesTooltip() {
if (this.changesCount === 0) return undefined;
if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
} else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
}
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
unstaged: this.folderUnstagedCount,
staged: this.folderStagedCount,
});
},
isTree() { isTree() {
return this.file.type === 'tree'; return this.file.type === 'tree';
}, },
...@@ -48,23 +88,30 @@ export default { ...@@ -48,23 +88,30 @@ export default {
'is-open': this.file.opened, 'is-open': this.file.opened,
}; };
}, },
showTreeChangesCount() {
return this.isTree && this.changesCount > 0 && !this.file.opened;
},
showChangedFileIcon() {
return this.file.changed || this.file.tempFile || this.file.staged;
},
}, },
updated() { updated() {
if (this.file.type === 'blob' && this.file.active) { if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView(); this.$el.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
} }
}, },
methods: { methods: {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']), ...mapActions(['toggleTreeOpen']),
clickFile() { clickFile() {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) { if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path); this.toggleTreeOpen(this.file.path);
} }
return this.updateDelayViewerUpdated(true).then(() => { router.push(`/project${this.file.url}`);
router.push(`/project${this.file.url}`);
});
}, },
}, },
}; };
...@@ -101,8 +148,23 @@ export default { ...@@ -101,8 +148,23 @@ export default {
<mr-file-icon <mr-file-icon
v-if="file.mrChange" v-if="file.mrChange"
/> />
<span
v-if="showTreeChangesCount"
class="ide-tree-changes"
>
{{ changesCount }}
<icon
v-tooltip
:title="folderChangesTooltip"
data-container="body"
data-placement="right"
name="file-modified"
:size="12"
css-classes="prepend-left-5 multi-file-modified"
/>
</span>
<changed-file-icon <changed-file-icon
v-if="file.changed || file.tempFile || file.staged" v-else-if="showChangedFileIcon"
:file="file" :file="file"
:show-tooltip="true" :show-tooltip="true"
:show-staged-icon="true" :show-staged-icon="true"
...@@ -111,7 +173,7 @@ export default { ...@@ -111,7 +173,7 @@ export default {
/> />
</span> </span>
<new-dropdown <new-dropdown
v-if="isTree" v-if="isTree && !disableActionDropdown"
:project-id="file.projectId" :project-id="file.projectId"
:branch="file.branchId" :branch="file.branchId"
:path="file.path" :path="file.path"
......
...@@ -32,6 +32,8 @@ export default { ...@@ -32,6 +32,8 @@ export default {
return `Close ${this.tab.name}`; return `Close ${this.tab.name}`;
}, },
showChangedIcon() { showChangedIcon() {
if (this.tab.pending) return true;
return this.fileHasChanged ? !this.tabMouseOver : false; return this.fileHasChanged ? !this.tabMouseOver : false;
}, },
fileHasChanged() { fileHasChanged() {
...@@ -66,15 +68,32 @@ export default { ...@@ -66,15 +68,32 @@ export default {
<template> <template>
<li <li
:class="{
active: tab.active
}"
@click="clickFile(tab)" @click="clickFile(tab)"
@mouseover="mouseOverTab" @mouseover="mouseOverTab"
@mouseout="mouseOutTab" @mouseout="mouseOutTab"
> >
<div
class="multi-file-tab"
:title="tab.url"
>
<file-icon
:file-name="tab.name"
:size="16"
/>
{{ tab.name }}
<file-status-icon
:file="tab"
/>
</div>
<button <button
type="button" type="button"
class="multi-file-tab-close" class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)" @click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel" :aria-label="closeLabel"
:disabled="tab.pending"
> >
<icon <icon
v-if="!showChangedIcon" v-if="!showChangedIcon"
...@@ -87,22 +106,5 @@ export default { ...@@ -87,22 +106,5 @@ export default {
:force-modified-icon="true" :force-modified-icon="true"
/> />
</button> </button>
<div
class="multi-file-tab"
:class="{
active: tab.active
}"
:title="tab.url"
>
<file-icon
:file-name="tab.name"
:size="16"
/>
{{ tab.name }}
<file-status-icon
:file="tab"
/>
</div>
</li> </li>
</template> </template>
...@@ -32,16 +32,6 @@ export default { ...@@ -32,16 +32,6 @@ export default {
default: '', default: '',
}, },
}, },
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: { methods: {
...mapActions(['updateViewer', 'removePendingTab']), ...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) { openFileViewer(viewer) {
...@@ -71,12 +61,5 @@ export default { ...@@ -71,12 +61,5 @@ export default {
:tab="tab" :tab="tab"
/> />
</ul> </ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
:merge-request-id="mergeRequestId"
@click="openFileViewer"
/>
</div> </div>
</template> </template>
...@@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40; ...@@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55; export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
export const COMMIT_ITEM_PADDING = 32;
// Commit message textarea // Commit message textarea
export const MAX_TITLE_LENGTH = 50; export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72; export const MAX_BODY_LENGTH = 72;
export const activityBarViews = {
edit: 'ide-tree',
commit: 'commit-section',
review: 'ide-review',
};
export const viewerTypes = {
mr: 'mrdiff',
edit: 'editor',
diff: 'diff',
};
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import flash from '~/flash'; import flash from '~/flash';
import store from './stores'; import store from './stores';
import { activityBarViews } from './constants';
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => { ...@@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`; const fullProjectId = `${to.params.namespace}/${to.params.project}`;
if (to.params.branch) { if (to.params.branch) {
store.dispatch('setCurrentBranchId', to.params.branch);
store.dispatch('getBranchData', { store.dispatch('getBranchData', {
projectId: fullProjectId, projectId: fullProjectId,
branchId: to.params.branch, branchId: to.params.branch,
...@@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => { ...@@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => {
throw e; throw e;
}); });
} else if (to.params.mrid) { } else if (to.params.mrid) {
store.dispatch('updateViewer', 'mrdiff');
store store
.dispatch('getMergeRequestData', { .dispatch('getMergeRequestData', {
projectId: fullProjectId, projectId: fullProjectId,
mergeRequestId: to.params.mrid, mergeRequestId: to.params.mrid,
}) })
.then(mr => { .then(mr => {
store.dispatch('updateActivityBarView', activityBarViews.review);
store.dispatch('getBranchData', { store.dispatch('getBranchData', {
projectId: fullProjectId, projectId: fullProjectId,
branchId: mr.source_branch, branchId: mr.source_branch,
......
...@@ -4,7 +4,9 @@ import ide from './components/ide.vue'; ...@@ -4,7 +4,9 @@ import ide from './components/ide.vue';
import store from './stores'; import store from './stores';
import router from './ide_router'; import router from './ide_router';
function initIde(el) { Vue.use(Translate);
export function initIde(el) {
if (!el) return null; if (!el) return null;
return new Vue({ return new Vue({
...@@ -14,20 +16,25 @@ function initIde(el) { ...@@ -14,20 +16,25 @@ function initIde(el) {
components: { components: {
ide, ide,
}, },
render(createElement) { created() {
return createElement('ide', { this.$store.dispatch('setEmptyStateSvgs', {
props: { emptyStateSvgPath: el.dataset.emptyStateSvgPath,
emptyStateSvgPath: el.dataset.emptyStateSvgPath, noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath, committedStateSvgPath: el.dataset.committedStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath,
},
}); });
}, },
render(createElement) {
return createElement('ide');
},
}); });
} }
const ideElement = document.getElementById('ide'); // tell webpack to load assets from origin so that web workers don't break
export function resetServiceWorkersPublicPath() {
Vue.use(Translate); // __webpack_public_path__ is a global variable that can be used to adjust
// the webpack publicPath setting at runtime.
initIde(ideElement); // see: https://webpack.js.org/guides/public-path/
const relativeRootPath = (gon && gon.relative_url_root) || '';
const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
__webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
}
...@@ -61,19 +61,19 @@ export default class Editor { ...@@ -61,19 +61,19 @@ export default class Editor {
} }
} }
createDiffInstance(domElement) { createDiffInstance(domElement, readOnly = true) {
if (!this.instance) { if (!this.instance) {
clearDomElement(domElement); clearDomElement(domElement);
this.disposable.add( this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, { (this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions, ...defaultEditorOptions,
readOnly: true,
quickSuggestions: false, quickSuggestions: false,
occurrencesHighlight: false, occurrencesHighlight: false,
renderLineHighlight: 'none',
hideCursorInOverviewRuler: true,
renderSideBySide: Editor.renderSideBySide(domElement), renderSideBySide: Editor.renderSideBySide(domElement),
readOnly,
renderLineHighlight: readOnly ? 'all' : 'none',
hideCursorInOverviewRuler: !readOnly,
})), })),
); );
......
...@@ -123,6 +123,8 @@ export const scrollToTab = () => { ...@@ -123,6 +123,8 @@ export const scrollToTab = () => {
}; };
export const stageAllChanges = ({ state, commit }) => { export const stageAllChanges = ({ state, commit }) => {
commit(types.SET_LAST_COMMIT_MSG, '');
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path)); state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
}; };
...@@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay); commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
}; };
export const updateActivityBarView = ({ commit }, view) => {
commit(types.UPDATE_ACTIVITY_BAR_VIEW, view);
};
export const setEmptyStateSvgs = ({ commit }, svgs) => {
commit(types.SET_EMPTY_STATE_SVGS, svgs);
};
export const setCurrentBranchId = ({ commit }, currentBranchId) => {
commit(types.SET_CURRENT_BRANCH, currentBranchId);
};
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => { export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile }); commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
......
...@@ -5,6 +5,7 @@ import service from '../../services'; ...@@ -5,6 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { setPageTitle } from '../utils'; import { setPageTitle } from '../utils';
import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => { export const closeFile = ({ commit, state, dispatch }, file) => {
const path = file.path; const path = file.path;
...@@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => { ...@@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
const nextFileToOpen = state.openFiles[nextIndexToOpen]; const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) { if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff'); dispatch('updateViewer', viewerTypes.diff);
dispatch('openPendingTab', { dispatch('openPendingTab', {
file: nextFileToOpen, file: nextFileToOpen,
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged', keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
}); });
} else { } else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`); router.push(`/project${nextFileToOpen.url}`);
} }
} else if (!state.openFiles.length) { } else if (!state.openFiles.length) {
...@@ -184,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => { ...@@ -184,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path); commit(types.STAGE_CHANGE, path);
commit(types.SET_LAST_COMMIT_MSG, '');
if (stagedFile) { if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content); eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
...@@ -195,9 +196,7 @@ export const unstageChange = ({ commit }, path) => { ...@@ -195,9 +196,7 @@ export const unstageChange = ({ commit }, path) => {
}; };
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => { export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') { state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
return false;
}
commit(types.ADD_PENDING_TAB, { file, keyPrefix }); commit(types.ADD_PENDING_TAB, { file, keyPrefix });
......
...@@ -55,7 +55,6 @@ export const getBranchData = ( ...@@ -55,7 +55,6 @@ export const getBranchData = (
branch: data, branch: data,
}); });
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
...@@ -73,3 +72,26 @@ export const getBranchData = ( ...@@ -73,3 +72,26 @@ export const getBranchData = (
resolve(state.projects[`${projectId}`].branches[branchId]); resolve(state.projects[`${projectId}`].branches[branchId]);
} }
}); });
export const refreshLastCommitData = (
{ commit, state, dispatch },
{ projectId, branchId } = {},
) => service
.getBranchData(projectId, branchId)
.then(({ data }) => {
commit(types.SET_BRANCH_COMMIT, {
projectId,
branchId,
commit: data.commit,
});
})
.catch(() => {
flash(
'Error loading last commit.',
'alert',
document,
null,
false,
true,
);
});
import { __ } from '~/locale'; import { getChangesCountForFiles, filePathMatches } from './utils';
import { activityBarViews } from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state => state.openFiles.find(file => file.active) || null;
...@@ -30,15 +31,12 @@ export const currentMergeRequest = state => { ...@@ -30,15 +31,12 @@ export const currentMergeRequest = state => {
return null; return null;
}; };
// eslint-disable-next-line no-confusing-arrow export const currentProject = state => state.projects[state.currentProjectId];
export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length; export const currentTree = state =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
// eslint-disable-next-line no-confusing-arrow export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
export const collapseButtonTooltip = state =>
state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
export const hasMergeRequest = state => !!state.currentMergeRequestId; export const hasMergeRequest = state => !!state.currentMergeRequestId;
...@@ -55,7 +53,39 @@ export const allBlobs = state => ...@@ -55,7 +53,39 @@ export const allBlobs = state =>
}, []) }, [])
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path); export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
export const lastOpenedFile = state =>
[...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0];
export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit;
export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
export const someUncommitedChanges = state =>
!!(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
const stagedFilesCount = state.stagedFiles.filter(
f => filePathMatches(f, path) && !getChangedFile(state)(f.path),
).length;
return changedFilesCount + stagedFilesCount;
};
export const getUnstagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.changedFiles, path);
export const getStagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.stagedFiles, path);
export const lastCommit = (state, getters) => {
const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId];
return branch ? branch.commit : null;
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -8,6 +8,7 @@ import router from '../../../ide_router'; ...@@ -8,6 +8,7 @@ import router from '../../../ide_router';
import service from '../../../services'; import service from '../../../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
import * as consts from './constants'; import * as consts from './constants';
import { activityBarViews } from '../../../constants';
import eventHub from '../../../eventhub'; import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => { export const updateCommitMessage = ({ commit }, message) => {
...@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) => ...@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) =>
export const updateFilesAfterCommit = ( export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters }, { commit, dispatch, state, rootState, rootGetters },
{ data, branch }, { data },
) => { ) => {
const selectedProject = rootState.projects[rootState.currentProjectId]; const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = { const lastCommit = {
...@@ -126,15 +127,9 @@ export const updateFilesAfterCommit = ( ...@@ -126,15 +127,9 @@ export const updateFilesAfterCommit = (
changed: !!changedFile, changed: !!changedFile,
}); });
}); });
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) {
router.push(
`/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
);
}
}; };
export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => { export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState); const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus'); const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
...@@ -187,7 +182,39 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = ...@@ -187,7 +182,39 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000); }, 5000);
}) })
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)); .then(() => {
if (rootGetters.lastOpenedFile) {
dispatch(
'openPendingTab',
{
file: rootGetters.lastOpenedFile,
},
{ root: true },
)
.then(changeViewer => {
if (changeViewer) {
dispatch('updateViewer', 'diff', { root: true });
}
})
.catch(e => {
throw e;
});
} else {
dispatch('updateActivityBarView', activityBarViews.edit, { root: true });
dispatch('updateViewer', 'editor', { root: true });
router.push(
`/project/${rootState.currentProjectId}/blob/${getters.branchName}/${
rootGetters.activeFile.path
}`,
);
}
})
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
.then(() => dispatch('refreshLastCommitData', {
projectId: rootState.currentProjectId,
branchId: rootState.currentBranchId,
}, { root: true }));
}) })
.catch(err => { .catch(err => {
let errMsg = __('Error committing changes. Please try again.'); let errMsg = __('Error committing changes. Please try again.');
......
...@@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG'; ...@@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
// Project Mutation Types // Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECT = 'SET_PROJECT';
...@@ -19,6 +20,7 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS'; ...@@ -19,6 +20,7 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types // Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH'; export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
...@@ -59,6 +61,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; ...@@ -59,6 +61,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
...@@ -107,6 +107,21 @@ export default { ...@@ -107,6 +107,21 @@ export default {
delayViewerUpdated, delayViewerUpdated,
}); });
}, },
[types.UPDATE_ACTIVITY_BAR_VIEW](state, currentActivityView) {
Object.assign(state, {
currentActivityView,
});
},
[types.SET_EMPTY_STATE_SVGS](
state,
{ emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
) {
Object.assign(state, {
emptyStateSvgPath,
noChangesStateSvgPath,
committedStateSvgPath,
});
},
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) { [types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
Object.assign(state, { Object.assign(state, {
fileFindVisible, fileFindVisible,
......
...@@ -23,4 +23,9 @@ export default { ...@@ -23,4 +23,9 @@ export default {
workingReference: reference, workingReference: reference,
}); });
}, },
[types.SET_BRANCH_COMMIT](state, { projectId, branchId, commit }) {
Object.assign(state.projects[projectId].branches[branchId], {
commit,
});
},
}; };
/* eslint-disable no-param-reassign */
import * as types from '../mutation_types'; import * as types from '../mutation_types';
export default { export default {
...@@ -169,32 +170,24 @@ export default { ...@@ -169,32 +170,24 @@ export default {
}); });
}, },
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) { [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
const key = `${keyPrefix}-${file.key}`; state.entries[file.path].opened = false;
const pendingTab = state.openFiles.find(f => f.key === key && f.pending); state.entries[file.path].active = false;
let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false })); state.entries[file.path].lastOpenedAt = new Date().getTime();
state.openFiles.forEach(f =>
if (!pendingTab) { Object.assign(f, {
const openFile = openFiles.find(f => f.path === file.path); opened: false,
active: false,
openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => { }),
if (!f) return acc; );
state.openFiles = [
if (f.path === file.path) { {
return acc.concat({ ...file,
...f, key: `${keyPrefix}-${file.key}`,
content: file.content, pending: true,
active: true, opened: true,
pending: true, active: true,
opened: true, },
key, ];
});
}
return acc.concat(f);
}, []);
}
Object.assign(state, { openFiles });
}, },
[types.REMOVE_PENDING_TAB](state, file) { [types.REMOVE_PENDING_TAB](state, file) {
Object.assign(state, { Object.assign(state, {
......
import { activityBarViews, viewerTypes } from '../constants';
export default () => ({ export default () => ({
currentProjectId: '', currentProjectId: '',
currentBranchId: '', currentBranchId: '',
...@@ -16,8 +18,9 @@ export default () => ({ ...@@ -16,8 +18,9 @@ export default () => ({
rightPanelCollapsed: false, rightPanelCollapsed: false,
panelResizing: false, panelResizing: false,
entries: {}, entries: {},
viewer: 'editor', viewer: viewerTypes.edit,
delayViewerUpdated: false, delayViewerUpdated: false,
currentActivityView: activityBarViews.edit,
unusedSeal: true, unusedSeal: true,
fileFindVisible: false, fileFindVisible: false,
}); });
...@@ -33,7 +33,6 @@ export const dataStructure = () => ({ ...@@ -33,7 +33,6 @@ export const dataStructure = () => ({
raw: '', raw: '',
content: '', content: '',
parentTreeUrl: '', parentTreeUrl: '',
parentPath: '',
renderError: false, renderError: false,
base64: false, base64: false,
editorRow: 1, editorRow: 1,
...@@ -43,6 +42,7 @@ export const dataStructure = () => ({ ...@@ -43,6 +42,7 @@ export const dataStructure = () => ({
viewMode: 'edit', viewMode: 'edit',
previewMode: null, previewMode: null,
size: 0, size: 0,
parentPath: null,
lastOpenedAt: 0, lastOpenedAt: 0,
}); });
...@@ -83,7 +83,6 @@ export const decorateData = entity => { ...@@ -83,7 +83,6 @@ export const decorateData = entity => {
opened, opened,
active, active,
parentTreeUrl, parentTreeUrl,
parentPath,
changed, changed,
renderError, renderError,
content, content,
...@@ -91,6 +90,7 @@ export const decorateData = entity => { ...@@ -91,6 +90,7 @@ export const decorateData = entity => {
previewMode, previewMode,
file_lock, file_lock,
html, html,
parentPath,
}; };
}; };
...@@ -137,3 +137,9 @@ export const sortTree = sortedTree => ...@@ -137,3 +137,9 @@ export const sortTree = sortedTree =>
}), }),
) )
.sort(sortTreesByTypeAndName); .sort(sortTreesByTypeAndName);
export const filePathMatches = (f, path) =>
f.path.replace(new RegExp(`${f.name}$`), '').indexOf(`${path}/`) === 0;
export const getChangesCountForFiles = (files, path) =>
files.filter(f => filePathMatches(f, path)).length;
...@@ -30,7 +30,7 @@ export default class IssuableForm { ...@@ -30,7 +30,7 @@ export default class IssuableForm {
} }
this.initAutosave(); this.initAutosave();
this.form.on('submit', this.handleSubmit); this.form.on('submit:success', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave); this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip(); this.initWip();
......
/* eslint-disable import/first */
/* global $ */ /* global $ */
import jQuery from 'jquery'; import jQuery from 'jquery';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import svg4everybody from 'svg4everybody'; import svg4everybody from 'svg4everybody';
// expose common libraries as globals (TODO: remove these) // bootstrap webpack, common libs, polyfills, and behaviors
window.jQuery = jQuery; import './webpack';
window.$ = jQuery; import './commons';
import './behaviors';
// lib/utils // lib/utils
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils'; import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// behaviors
import './behaviors/';
// everything else // everything else
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import bp from './breakpoints'; import bp from './breakpoints';
...@@ -31,9 +28,12 @@ import initLogoAnimation from './logo'; ...@@ -31,9 +28,12 @@ import initLogoAnimation from './logo';
import './milestone_select'; import './milestone_select';
import './projects_dropdown'; import './projects_dropdown';
import initBreadcrumbs from './breadcrumb'; import initBreadcrumbs from './breadcrumb';
import initDispatcher from './dispatcher'; import initDispatcher from './dispatcher';
// expose jQuery as global (TODO: remove these)
window.jQuery = jQuery;
window.$ = jQuery;
// inject test utilities if necessary // inject test utilities if necessary
if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) { if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
$.fx.off = true; $.fx.off = true;
...@@ -52,10 +52,14 @@ document.addEventListener('beforeunload', () => { ...@@ -52,10 +52,14 @@ document.addEventListener('beforeunload', () => {
}); });
window.addEventListener('hashchange', handleLocationHash); window.addEventListener('hashchange', handleLocationHash);
window.addEventListener('load', function onLoad() { window.addEventListener(
window.removeEventListener('load', onLoad, false); 'load',
handleLocationHash(); function onLoad() {
}, false); window.removeEventListener('load', onLoad, false);
handleLocationHash();
},
false,
);
gl.lazyLoader = new LazyLoader({ gl.lazyLoader = new LazyLoader({
scrollContainer: window, scrollContainer: window,
...@@ -89,9 +93,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -89,9 +93,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (bootstrapBreakpoint === 'xs') { if (bootstrapBreakpoint === 'xs') {
const $rightSidebar = $('aside.right-sidebar, .layout-page'); const $rightSidebar = $('aside.right-sidebar, .layout-page');
$rightSidebar $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
.removeClass('right-sidebar-expanded')
.addClass('right-sidebar-collapsed');
} }
// prevent default action for disabled buttons // prevent default action for disabled buttons
...@@ -108,7 +110,8 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -108,7 +110,8 @@ document.addEventListener('DOMContentLoaded', () => {
addSelectOnFocusBehaviour('.js-select-on-focus'); addSelectOnFocusBehaviour('.js-select-on-focus');
$('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() {
$(this).tooltip('destroy') $(this)
.tooltip('destroy')
.closest('li') .closest('li')
.fadeOut(); .fadeOut();
}); });
...@@ -118,7 +121,9 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -118,7 +121,9 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
$('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() { $('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() {
$(this).closest('tr').fadeOut(); $(this)
.closest('tr')
.fadeOut();
}); });
// Initialize select2 selects // Initialize select2 selects
...@@ -155,7 +160,9 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -155,7 +160,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Form submitter // Form submitter
$('.trigger-submit').on('change', function triggerSubmitCallback() { $('.trigger-submit').on('change', function triggerSubmitCallback() {
$(this).parents('form').submit(); $(this)
.parents('form')
.submit();
}); });
localTimeAgo($('abbr.timeago, .js-timeago'), true); localTimeAgo($('abbr.timeago, .js-timeago'), true);
...@@ -204,9 +211,15 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -204,9 +211,15 @@ document.addEventListener('DOMContentLoaded', () => {
$this.toggleClass('active'); $this.toggleClass('active');
if ($this.hasClass('active')) { if ($this.hasClass('active')) {
notesHolders.show().find('.hide, .content').show(); notesHolders
.show()
.find('.hide, .content')
.show();
} else { } else {
notesHolders.hide().find('.content').hide(); notesHolders
.hide()
.find('.content')
.hide();
} }
$(document).trigger('toggle.comments'); $(document).trigger('toggle.comments');
...@@ -247,9 +260,11 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -247,9 +260,11 @@ document.addEventListener('DOMContentLoaded', () => {
const flashContainer = document.querySelector('.flash-container'); const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) { if (flashContainer && flashContainer.children.length) {
flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => { flashContainer
removeFlashClickListener(flashEl); .querySelectorAll('.flash-alert, .flash-notice, .flash-success')
}); .forEach(flashEl => {
removeFlashClickListener(flashEl);
});
} }
initDispatcher(); initDispatcher();
......
/* eslint-disable no-new */
import $ from 'jquery'; import $ from 'jquery';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
...@@ -62,7 +60,7 @@ export default class MiniPipelineGraph { ...@@ -62,7 +60,7 @@ export default class MiniPipelineGraph {
*/ */
renderBuildsList(stageContainer, data) { renderBuildsList(stageContainer, data) {
const dropdownContainer = stageContainer.parentElement.querySelector( const dropdownContainer = stageContainer.parentElement.querySelector(
`${this.dropdownListSelector} .js-builds-dropdown-list`, `${this.dropdownListSelector} .js-builds-dropdown-list ul`,
); );
dropdownContainer.innerHTML = data; dropdownContainer.innerHTML = data;
......
...@@ -81,9 +81,8 @@ export default { ...@@ -81,9 +81,8 @@ export default {
time: new Date(), time: new Date(),
value: 0, value: 0,
}, },
currentDataIndex: 0,
currentXCoordinate: 0, currentXCoordinate: 0,
currentFlagPosition: 0, currentCoordinates: [],
showFlag: false, showFlag: false,
showFlagContent: false, showFlagContent: false,
timeSeries: [], timeSeries: [],
...@@ -273,6 +272,9 @@ export default { ...@@ -273,6 +272,9 @@ export default {
:line-style="path.lineStyle" :line-style="path.lineStyle"
:line-color="path.lineColor" :line-color="path.lineColor"
:area-color="path.areaColor" :area-color="path.areaColor"
:current-coordinates="currentCoordinates[index]"
:current-time-series-index="index"
:show-dot="showFlagContent"
/> />
<graph-deployment <graph-deployment
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
...@@ -298,9 +300,9 @@ export default { ...@@ -298,9 +300,9 @@ export default {
:show-flag-content="showFlagContent" :show-flag-content="showFlagContent"
:time-series="timeSeries" :time-series="timeSeries"
:unit-of-display="unitOfDisplay" :unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
:legend-title="legendTitle" :legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData" :deployment-flag-data="deploymentFlagData"
:current-coordinates="currentCoordinates"
/> />
</div> </div>
<graph-legend <graph-legend
......
...@@ -47,14 +47,14 @@ export default { ...@@ -47,14 +47,14 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: { legendTitle: {
type: String, type: String,
required: true, required: true,
}, },
currentCoordinates: {
type: Array,
required: true,
},
}, },
computed: { computed: {
formatTime() { formatTime() {
...@@ -90,10 +90,12 @@ export default { ...@@ -90,10 +90,12 @@ export default {
}, },
}, },
methods: { methods: {
seriesMetricValue(series) { seriesMetricValue(seriesIndex, series) {
const indexFromCoordinates = this.currentCoordinates[seriesIndex]
? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
const index = this.deploymentFlagData const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex ? this.deploymentFlagData.seriesIndex
: this.currentDataIndex; : indexFromCoordinates;
const value = series.values[index] && series.values[index].value; const value = series.values[index] && series.values[index].value;
if (isNaN(value)) { if (isNaN(value)) {
return '-'; return '-';
...@@ -128,7 +130,7 @@ export default { ...@@ -128,7 +130,7 @@ export default {
<h5 v-if="deploymentFlagData"> <h5 v-if="deploymentFlagData">
Deployed Deployed
</h5> </h5>
{{ formatDate }} at {{ formatDate }}
<strong>{{ formatTime }}</strong> <strong>{{ formatTime }}</strong>
</div> </div>
<div <div
...@@ -163,9 +165,11 @@ export default { ...@@ -163,9 +165,11 @@ export default {
:key="index" :key="index"
> >
<track-line :track="series"/> <track-line :track="series"/>
<td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
<td> <td>
<strong>{{ seriesMetricValue(series) }}</strong> {{ series.track }} {{ seriesMetricLabel(index, series) }}
</td>
<td>
<strong>{{ seriesMetricValue(index, series) }}</strong>
</td> </td>
</tr> </tr>
</table> </table>
......
...@@ -22,6 +22,15 @@ export default { ...@@ -22,6 +22,15 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
currentCoordinates: {
type: Object,
required: false,
default: () => ({ currentX: 0, currentY: 0 }),
},
showDot: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
strokeDashArray() { strokeDashArray() {
...@@ -33,12 +42,20 @@ export default { ...@@ -33,12 +42,20 @@ export default {
}; };
</script> </script>
<template> <template>
<g> <g transform="translate(-5, 20)">
<circle
class="circle-path"
:cx="currentCoordinates.currentX"
:cy="currentCoordinates.currentY"
:fill="lineColor"
:stroke="lineColor"
r="3"
v-if="showDot"
/>
<path <path
class="metric-area" class="metric-area"
:d="generatedAreaPath" :d="generatedAreaPath"
:fill="areaColor" :fill="areaColor"
transform="translate(-5, 20)"
/> />
<path <path
class="metric-line" class="metric-line"
...@@ -47,7 +64,6 @@ export default { ...@@ -47,7 +64,6 @@ export default {
fill="none" fill="none"
stroke-width="1" stroke-width="1"
:stroke-dasharray="strokeDashArray" :stroke-dasharray="strokeDashArray"
transform="translate(-5, 20)"
/> />
</g> </g>
</template> </template>
...@@ -19,16 +19,16 @@ export default { ...@@ -19,16 +19,16 @@ export default {
<template> <template>
<td> <td>
<svg <svg
width="15" width="16"
height="6"> height="8">
<line <line
:stroke-dasharray="stylizedLine" :stroke-dasharray="stylizedLine"
:stroke="track.lineColor" :stroke="track.lineColor"
stroke-width="4" stroke-width="4"
:x1="0" :x1="0"
:x2="15" :x2="16"
:y1="2" :y1="4"
:y2="2" :y2="4"
/> />
</svg> </svg>
</td> </td>
......
...@@ -52,14 +52,22 @@ const mixins = { ...@@ -52,14 +52,22 @@ const mixins = {
positionFlag() { positionFlag() {
const timeSeries = this.timeSeries[0]; const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1); const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
this.currentData = timeSeries.values[hoveredDataIndex]; this.currentData = timeSeries.values[hoveredDataIndex];
this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time)); this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103; this.currentCoordinates = this.timeSeries.map((series) => {
} else { const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
this.currentFlagPosition = this.currentXCoordinate; const currentData = series.values[currentDataIndex];
} const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
return {
currentX,
currentY,
currentDataIndex,
};
});
if (this.hoverData.currentDeployXPos) { if (this.hoverData.currentDeployXPos) {
this.showFlag = false; this.showFlag = false;
......
...@@ -14,7 +14,7 @@ const d3 = { ...@@ -14,7 +14,7 @@ const d3 = {
timeYear, timeYear,
}; };
export const dateFormat = d3.time('%a, %b %-d'); export const dateFormat = d3.time('%d %b %Y, ');
export const timeFormat = d3.time('%-I:%M%p'); export const timeFormat = d3.time('%-I:%M%p');
export const dateFormatWithName = d3.time('%a, %b %-d'); export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left; export const bisectDate = d3.bisector(d => d.time).left;
......
...@@ -123,6 +123,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -123,6 +123,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
linePath: lineFunction(timeSeries.values), linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values), areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX, timeSeriesScaleX,
timeSeriesScaleY,
values: timeSeries.values, values: timeSeries.values,
max: maximumValue, max: maximumValue,
average: accum / timeSeries.values.length, average: accum / timeSeries.values.length,
......
...@@ -99,10 +99,6 @@ export default { ...@@ -99,10 +99,6 @@ export default {
'js-note-target-reopen': !this.isOpen, 'js-note-target-reopen': !this.isOpen,
}; };
}, },
supportQuickActions() {
// Disable quick actions support for Epics
return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
},
markdownDocsPath() { markdownDocsPath() {
return this.getNotesData.markdownDocsPath; return this.getNotesData.markdownDocsPath;
}, },
...@@ -359,7 +355,7 @@ Please check your network connection and try again.`; ...@@ -359,7 +355,7 @@ Please check your network connection and try again.`;
name="note[note]" name="note[note]"
class="note-textarea js-vue-comment-form class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea" js-gfm-input js-autosize markdown-area js-vue-textarea"
:data-supports-quick-actions="supportQuickActions" data-supports-quick-actions="true"
aria-label="Description" aria-label="Description"
v-model="note" v-model="note"
ref="textarea" ref="textarea"
......
import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
const variableListEl = document.querySelector('.js-ci-variable-list-section'); const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new AjaxVariableList({ new AjaxVariableList({
......
import { initIde, resetServiceWorkersPublicPath } from '~/ide/index';
document.addEventListener('DOMContentLoaded', () => {
const ideElement = document.getElementById('ide');
if (ideElement) {
resetServiceWorkersPublicPath();
initIde(ideElement);
}
});
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
gcpSignupOffer();
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
gcpSignupOffer();
import Diff from '~/diff'; import Diff from '~/diff';
import initChangesDropdown from '~/init_changes_dropdown'; import initChangesDropdown from '~/init_changes_dropdown';
import GpgBadges from '~/gpg_badges';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new new Diff(); // eslint-disable-line no-new
const paddingTop = 16; const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
GpgBadges.fetch();
}); });
import $ from 'jquery'; import $ from 'jquery';
import NewBranchForm from '~/new_branch_form'; import NewBranchForm from '~/new_branch_form';
import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
setupNativeFormVariableList({
container: $('.js-ci-variable-list-section'),
formField: 'variables_attributes',
});
}); });
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
methods: { methods: {
onClickAction() { onClickAction() {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link); eventHub.$emit('postAction', this.link);
this.linkRequested = this.link; this.linkRequested = this.link;
this.isDisabled = true; this.isDisabled = true;
}, },
......
...@@ -87,7 +87,8 @@ export default { ...@@ -87,7 +87,8 @@ export default {
data-toggle="dropdown" data-toggle="dropdown"
data-container="body" data-container="body"
class="dropdown-menu-toggle build-content" class="dropdown-menu-toggle build-content"
:title="tooltipText"> :title="tooltipText"
>
<job-name-component <job-name-component
:name="job.name" :name="job.name"
...@@ -104,7 +105,8 @@ export default { ...@@ -104,7 +105,8 @@ export default {
<ul> <ul>
<li <li
v-for="(item, i) in job.jobs" v-for="(item, i) in job.jobs"
:key="i"> :key="i"
>
<job-component <job-component
:job="item" :job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
......
...@@ -108,7 +108,7 @@ export default { ...@@ -108,7 +108,7 @@ export default {
<div <div
v-else v-else
v-tooltip v-tooltip
class="js-job-component-tooltip" class="js-job-component-tooltip non-details-job-component"
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
data-html="true" data-html="true"
......
<script> <script>
/**
/** * Renders each stage of the pipeline mini graph.
* Renders each stage of the pipeline mini graph. *
* * Given the provided endpoint will make a request to
* Given the provided endpoint will make a request to * fetch the dropdown data when the stage is clicked.
* fetch the dropdown data when the stage is clicked. *
* * Request is made inside this component to make it reusable between:
* Request is made inside this component to make it reusable between: * 1. Pipelines main table
* 1. Pipelines main table * 2. Pipelines table in commit and Merge request views
* 2. Pipelines table in commit and Merge request views * 3. Merge request widget
* 3. Merge request widget * 4. Commit widget
* 4. Commit widget */
*/
import $ from 'jquery';
import $ from 'jquery'; import { __ } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils'; import axios from '../../lib/utils/axios_utils';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import JobComponent from './graph/job_component.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: { export default {
LoadingIcon, components: {
Icon, LoadingIcon,
Icon,
JobComponent,
},
directives: {
tooltip,
},
props: {
stage: {
type: Object,
required: true,
}, },
directives: { updateDropdown: {
tooltip, type: Boolean,
required: false,
default: false,
}, },
},
props: {
stage: { data() {
type: Object, return {
required: true, isLoading: false,
}, dropdownContent: '',
};
updateDropdown: { },
type: Boolean,
required: false, computed: {
default: false, dropdownClass() {
}, return this.dropdownContent.length > 0
? 'js-builds-dropdown-container'
: 'js-builds-dropdown-loading';
}, },
data() { triggerButtonClass() {
return { return `ci-status-icon-${this.stage.status.group}`;
isLoading: false,
dropdownContent: '',
};
}, },
computed: { borderlessIcon() {
dropdownClass() { return `${this.stage.status.icon}_borderless`;
return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading'; },
}, },
triggerButtonClass() { watch: {
return `ci-status-icon-${this.stage.status.group}`; updateDropdown() {
}, if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
this.fetchJobs();
}
},
},
updated() {
if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation();
}
},
methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
borderlessIcon() { fetchJobs() {
return `${this.stage.status.icon}_borderless`; axios
}, .get(this.stage.dropdown_path)
.then(({ data }) => {
this.dropdownContent = data.latest_statuses;
this.isLoading = false;
})
.catch(() => {
this.closeDropdown();
this.isLoading = false;
Flash(__('Something went wrong on our end.'));
});
}, },
watch: { /**
updateDropdown() { * When the user right clicks or cmd/ctrl + click in the job name
if (this.updateDropdown && * the dropdown should not be closed and the link should open in another tab,
this.isDropdownOpen() && * so we stop propagation of the click event inside the dropdown.
!this.isLoading) { *
this.fetchJobs(); * Since this component is rendered multiple times per page we need to guarantee we only
} * target the click event of this component.
}, */
stopDropdownClickPropagation() {
$(
'.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
this.$el,
).on('click', e => {
e.stopPropagation();
});
}, },
updated() { closeDropdown() {
if (this.dropdownContent.length > 0) { if (this.isDropdownOpen()) {
this.stopDropdownClickPropagation(); $(this.$refs.dropdown).dropdown('toggle');
} }
}, },
methods: { isDropdownOpen() {
onClickStage() { return this.$el.classList.contains('open');
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
fetchJobs() {
axios.get(this.stage.dropdown_path)
.then(({ data }) => {
this.dropdownContent = data.html;
this.isLoading = false;
})
.catch(() => {
this.closeDropdown();
this.isLoading = false;
Flash('Something went wrong on our end.');
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
isDropdownOpen() {
return this.$el.classList.contains('open');
},
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -168,7 +173,6 @@ ...@@ -168,7 +173,6 @@
> >
<li <li
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu" class="js-builds-dropdown-list scrollable-menu"
> >
...@@ -176,8 +180,16 @@ ...@@ -176,8 +180,16 @@
<ul <ul
v-else v-else
v-html="dropdownContent"
> >
<li
v-for="job in dropdownContent"
:key="job.id"
>
<job-component
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
/>
</li>
</ul> </ul>
</li> </li>
</ul> </ul>
......
...@@ -29,10 +29,10 @@ export default () => { ...@@ -29,10 +29,10 @@ export default () => {
}; };
}, },
created() { created() {
eventHub.$on('graphAction', this.postAction); eventHub.$on('postAction', this.postAction);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('graphAction', this.postAction); eventHub.$off('postAction', this.postAction);
}, },
methods: { methods: {
postAction(action) { postAction(action) {
......
...@@ -100,9 +100,10 @@ export default { ...@@ -100,9 +100,10 @@ export default {
fetchSearchedProjects(searchQuery) { fetchSearchedProjects(searchQuery) {
this.searchQuery = searchQuery; this.searchQuery = searchQuery;
this.toggleLoader(true); this.toggleLoader(true);
this.service.getSearchedProjects(this.searchQuery) this.service
.getSearchedProjects(this.searchQuery)
.then(res => res.json()) .then(res => res.json())
.then((results) => { .then(results => {
this.toggleSearchProjectsList(true); this.toggleSearchProjectsList(true);
this.store.setSearchedProjects(results); this.store.setSearchedProjects(results);
}) })
......
...@@ -50,7 +50,7 @@ export default class ProjectsService { ...@@ -50,7 +50,7 @@ export default class ProjectsService {
} else { } else {
// Check if project is already present in frequents list // Check if project is already present in frequents list
// When found, update metadata of it. // When found, update metadata of it.
storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => { storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
if (projectItem.id === project.id) { if (projectItem.id === project.id) {
matchFound = true; matchFound = true;
const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS; const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
...@@ -104,13 +104,17 @@ export default class ProjectsService { ...@@ -104,13 +104,17 @@ export default class ProjectsService {
return []; return [];
} }
if (bp.getBreakpointSize() === 'sm' || if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
bp.getBreakpointSize() === 'xs') {
frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE; frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
} }
const frequentProjects = storedFrequentProjects const frequentProjects = storedFrequentProjects.filter(
.filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY); project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
);
if (!frequentProjects || frequentProjects.length === 0) {
return [];
}
// Sort all frequent projects in decending order of frequency // Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first // and then by lastAccessedOn with recent most first
......
...@@ -85,6 +85,7 @@ export default class Shortcuts { ...@@ -85,6 +85,7 @@ export default class Shortcuts {
if ($modal.length) { if ($modal.length) {
$modal.modal('toggle'); $modal.modal('toggle');
return null;
} }
return axios.get(gon.shortcuts_path, { return axios.get(gon.shortcuts_path, {
......
...@@ -70,6 +70,9 @@ ...@@ -70,6 +70,9 @@
toggleMoreParticipants() { toggleMoreParticipants() {
this.isShowingMoreParticipants = !this.isShowingMoreParticipants; this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
}, },
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
},
}, },
}; };
</script> </script>
...@@ -82,6 +85,7 @@ ...@@ -82,6 +85,7 @@
data-container="body" data-container="body"
data-placement="left" data-placement="left"
:title="participantLabel" :title="participantLabel"
@click="onClickCollapsedIcon"
> >
<i <i
class="fa fa-users" class="fa fa-users"
......
<script> <script>
import Store from '../../stores/sidebar_store'; import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue'; import subscriptions from './subscriptions.vue';
...@@ -20,12 +19,6 @@ export default { ...@@ -20,12 +19,6 @@ export default {
store: new Store(), store: new Store(),
}; };
}, },
created() {
eventHub.$on('toggleSubscription', this.onToggleSubscription);
},
beforeDestroy() {
eventHub.$off('toggleSubscription', this.onToggleSubscription);
},
methods: { methods: {
onToggleSubscription() { onToggleSubscription() {
this.mediator.toggleSubscription() this.mediator.toggleSubscription()
...@@ -42,6 +35,7 @@ export default { ...@@ -42,6 +35,7 @@ export default {
<subscriptions <subscriptions
:loading="store.isFetching.subscriptions" :loading="store.isFetching.subscriptions"
:subscribed="store.subscribed" :subscribed="store.subscribed"
@toggleSubscription="onToggleSubscription"
/> />
</div> </div>
</template> </template>
...@@ -47,8 +47,25 @@ ...@@ -47,8 +47,25 @@
}, },
}, },
methods: { methods: {
/**
* We need to emit this event on both component & eventHub
* for 2 dependencies;
*
* 1. eventHub: This component is used in Issue Boards sidebar
* where component template is part of HAML
* and event listeners are tied to app's eventHub.
* 2. Component: This compone is also used in Epics in EE
* where listeners are tied to component event.
*/
toggleSubscription() { toggleSubscription() {
// App's eventHub event emission.
eventHub.$emit('toggleSubscription', this.id); eventHub.$emit('toggleSubscription', this.id);
// Component event emission.
this.$emit('toggleSubscription', this.id);
},
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
}, },
}, },
}; };
...@@ -56,7 +73,10 @@ ...@@ -56,7 +73,10 @@
<template> <template>
<div> <div>
<div class="sidebar-collapsed-icon"> <div
class="sidebar-collapsed-icon"
@click="onClickCollapsedIcon"
>
<span <span
v-tooltip v-tooltip
:title="notificationTooltip" :title="notificationTooltip"
......
<script> <script>
import TimeTrackingHelpState from './help_state.vue'; import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue'; import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane'; import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
import TimeTrackingNoTrackingPane from './no_tracking_pane.vue'; import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue'; import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue'; import TimeTrackingComparisonPane from './comparison_pane.vue';
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
components: { components: {
TimeTrackingCollapsedState, TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane, TimeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane, TimeTrackingSpentOnlyPane,
TimeTrackingNoTrackingPane, TimeTrackingNoTrackingPane,
TimeTrackingComparisonPane, TimeTrackingComparisonPane,
TimeTrackingHelpState, TimeTrackingHelpState,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment