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