Commit 9e6b31eb authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into 'ee-explore-dispatcher-refactor'

# Conflicts:
#   app/assets/javascripts/dispatcher.js
parents 4e1ecbc5 402c3d13
......@@ -13,7 +13,8 @@ engines:
exclude_paths:
- "lib/api/v3/*"
eslint:
enabled: true
# eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4
enabled: false
rubocop:
enabled: true
channel: "gitlab-rubocop-0-52"
......
......@@ -4,7 +4,10 @@
"browser": true,
"es6": true
},
"extends": "airbnb-base",
"extends": [
"airbnb-base",
"plugin:vue/recommended"
],
"globals": {
"__webpack_public_path__": true,
"_": false,
......@@ -12,7 +15,9 @@
"gon": false,
"localStorage": false
},
"parser": "babel-eslint",
"parserOptions": {
"parser": "babel-eslint"
},
"plugins": [
"filenames",
"import",
......@@ -20,7 +25,7 @@
"promise"
],
"settings": {
"html/html-extensions": [".html", ".html.raw", ".vue"],
"html/html-extensions": [".html", ".html.raw"],
"import/resolver": {
"webpack": {
"config": "./config/webpack.config.js"
......@@ -32,6 +37,15 @@
"import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__"]}]
"no-underscore-dangle": ["error", { "allow": ["__"]}],
"vue/html-self-closing": ["error", {
"html": {
"void": "always",
"normal": "never",
"component": "always"
},
"svg": "always",
"math": "always"
}]
}
}
......@@ -105,9 +105,13 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute!
If you want to contribute to GitLab, [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight] is a great place to start. Issues with a lower weight (1 or 2) are deemed suitable for beginners.
These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab.
If you want to contribute to GitLab [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight]
is a great place to start. Issues with a lower weight (1 or 2) are deemed
suitable for beginners. These issues will be of reasonable size and challenge,
for anyone to start contributing to GitLab. If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to
learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel
please consider we favor
[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution!
## Workflow labels
......
......@@ -73,6 +73,10 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
gem 'gollum-lib', '~> 4.2', require: false
# Before updating this gem, check if
# https://github.com/gollum/rugged_adapter/pull/28 has been merged.
# If it has, then remove the monkey patch for tree_entry in config/initializers/gollum.rb
gem 'gollum-rugged_adapter', '~> 0.4.4', require: false
# Language detection
......@@ -81,7 +85,7 @@ gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes'
......
......@@ -680,7 +680,7 @@ GEM
rack (>= 0.4)
rack-attack (4.4.1)
rack
rack-cors (0.4.0)
rack-cors (1.0.2)
rack-oauth2 (1.2.3)
activesupport (>= 2.3)
attr_required (>= 0.0.5)
......@@ -1171,7 +1171,7 @@ DEPENDENCIES
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
rack-cors (~> 0.4.0)
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0)
rails (= 4.2.10)
......@@ -1251,4 +1251,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.16.0
1.16.1
......@@ -89,40 +89,53 @@
<p>Track your GitLab projects with GitLab for Slack.</p>
</div>
<div class="append-bottom-20 center" v-once>
<div
class="append-bottom-20 center"
v-once
>
<img
class="gitlab-slack-logo"
:src="gitlabLogoPath"></img>
:src="gitlabLogoPath"
/>
<div
class="gitlab-slack-double-headed-arrow inline prepend-left-20 append-right-20"
v-html="doubleHeadedArrowSvg"></div>
v-html="doubleHeadedArrowSvg"
>
</div>
<img
class="gitlab-slack-logo"
:src="slackLogoPath"></img>
:src="slackLogoPath"
/>
</div>
<button
type="button"
class="btn btn-red center-block js-popup-button"
@click="togglePopup">
@click="togglePopup"
>
Add GitLab to Slack
</button>
<div
class="popup gitlab-slack-popup center-block prepend-top-20 text-center js-popup"
v-if="popupOpen">
v-if="popupOpen"
>
<div
class="inline"
v-if="isSignedIn && hasProjects">
v-if="isSignedIn && hasProjects"
>
<strong>Select GitLab project to link with your Slack team</strong>
<select
class="gitlab-slack-project-select js-project-select form-control prepend-top-10 append-bottom-10"
v-model="selectedProjectId">
class="gitlab-slack-project-select
js-project-select form-control prepend-top-10 append-bottom-10"
v-model="selectedProjectId"
>
<option
v-for="project in projects"
:key="project.id"
:value="project.id">
:value="project.id
">
{{ project.name }}
</option>
</select>
......@@ -130,14 +143,16 @@
<button
type="button"
class="btn btn-red pull-right js-add-button"
@click="addToSlack">
@click="addToSlack"
>
Add to Slack
</button>
</div>
<span
class="js-no-projects"
v-else-if="isSignedIn && !hasProjects">
v-else-if="isSignedIn && !hasProjects"
>
You don't have any projects available.
</span>
......@@ -146,7 +161,8 @@
<a
class="js-gitlab-slack-sign-in-link"
v-once
:href="signInPath">
:href="signInPath"
>
log in
</a>
</span>
......@@ -156,20 +172,26 @@
<img
v-once
class="gitlab-slack-gif"
:src="gitlabForSlackGifPath">
:src="gitlabForSlackGifPath"
/>
</div>
<div
class="gitlab-slack-example"
v-once>
v-once
>
<h3 class="center">How it works</h3>
<div class="well gitlab-slack-well center-block">
<code class="code center-block append-bottom-10">/gitlab &lt;project-alias&gt; issue show &lt;id&gt;</code>
<code
class="code center-block append-bottom-10"
>/gitlab &lt;project-alias&gt; issue show &lt;id&gt;</code>
<span>
<div
class="gitlab-slack-right-arrow inline append-right-5"
v-html="arrowRightSvg"></div>
v-html="arrowRightSvg"
>
</div>
Shows the issue with id
<strong>&lt;id&gt;</strong>
</span>
......
......@@ -7,6 +7,7 @@ import installGlEmojiElement from './gl_emoji';
import './quick_submit';
import './requires_input';
import './toggler_behavior';
import '../preview_markdown';
installGlEmojiElement();
initCopyAsGFM();
......
......@@ -8,6 +8,9 @@ export default () => {
new Vue({
el,
components: {
notebookLab,
},
data() {
return {
error: false,
......@@ -16,8 +19,41 @@ export default () => {
json: {},
};
},
components: {
notebookLab,
mounted() {
if (gon.katex_css_url) {
const katexStyles = document.createElement('link');
katexStyles.setAttribute('rel', 'stylesheet');
katexStyles.setAttribute('href', gon.katex_css_url);
document.head.appendChild(katexStyles);
}
if (gon.katex_js_url) {
const katexScript = document.createElement('script');
katexScript.addEventListener('load', () => {
this.loadFile();
});
katexScript.setAttribute('src', gon.katex_js_url);
document.head.appendChild(katexScript);
} else {
this.loadFile();
}
},
methods: {
loadFile() {
axios.get(el.dataset.endpoint)
.then(res => res.data)
.then((data) => {
this.json = data;
this.loading = false;
})
.catch((e) => {
if (e.status !== 200) {
this.loadError = true;
}
this.error = true;
});
},
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
......@@ -46,41 +82,5 @@ export default () => {
</p>
</div>
`,
methods: {
loadFile() {
axios.get(el.dataset.endpoint)
.then(res => res.data)
.then((data) => {
this.json = data;
this.loading = false;
})
.catch((e) => {
if (e.status !== 200) {
this.loadError = true;
}
this.error = true;
});
},
},
mounted() {
if (gon.katex_css_url) {
const katexStyles = document.createElement('link');
katexStyles.setAttribute('rel', 'stylesheet');
katexStyles.setAttribute('href', gon.katex_css_url);
document.head.appendChild(katexStyles);
}
if (gon.katex_js_url) {
const katexScript = document.createElement('script');
katexScript.addEventListener('load', () => {
this.loadFile();
});
katexScript.setAttribute('src', gon.katex_js_url);
document.head.appendChild(katexScript);
} else {
this.loadFile();
}
},
});
};
......@@ -7,6 +7,9 @@ export default () => {
return new Vue({
el,
components: {
pdfLab,
},
data() {
return {
error: false,
......@@ -15,9 +18,6 @@ export default () => {
pdf: el.dataset.endpoint,
};
},
components: {
pdfLab,
},
methods: {
onLoad() {
this.loading = false;
......
......@@ -208,6 +208,9 @@ $(() => {
if (configEl) {
gl.boardConfigToggle = new Vue({
el: configEl,
directives: {
tooltip,
},
data() {
return {
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
......@@ -215,12 +218,6 @@ $(() => {
state: Store.state,
};
},
directives: {
tooltip,
},
methods: {
showPage: page => gl.issueBoards.BoardsStore.showPage(page),
},
computed: {
buttonText() {
return this.canAdminList ? 'Edit board' : 'View scope';
......@@ -229,6 +226,9 @@ $(() => {
return this.hasScope ? __('This board\'s scope is reduced') : '';
}
},
methods: {
showPage: page => gl.issueBoards.BoardsStore.showPage(page),
},
template: `
<div class="prepend-left-10">
<button
......@@ -247,8 +247,8 @@ $(() => {
}
gl.IssueBoardsModalAddBtn = new Vue({
mixins: [gl.issueBoards.ModalMixins],
el: document.getElementById('js-add-issues-btn'),
mixins: [gl.issueBoards.ModalMixins],
data() {
return {
modal: ModalStore.store,
......@@ -258,11 +258,6 @@ $(() => {
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
};
},
watch: {
disabled() {
this.updateTooltip();
},
},
computed: {
disabled() {
if (!this.store) {
......@@ -278,6 +273,14 @@ $(() => {
return '';
},
},
watch: {
disabled() {
this.updateTooltip();
},
},
mounted() {
this.updateTooltip();
},
methods: {
updateTooltip() {
const $tooltip = $(this.$refs.addIssuesButton);
......@@ -296,9 +299,6 @@ $(() => {
}
},
},
mounted() {
this.updateTooltip();
},
template: `
<div class="board-extra-actions">
<button
......
......@@ -4,6 +4,10 @@ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
components: {
loadingIcon,
UserAvatarImage,
},
props: {
anyUserText: {
type: String,
......@@ -52,10 +56,6 @@ export default {
default: '',
},
},
components: {
loadingIcon,
UserAvatarImage,
},
computed: {
hasValue() {
return this.selected.id > 0;
......@@ -69,6 +69,9 @@ export default {
this.initSelect();
},
},
mounted() {
this.initSelect();
},
methods: {
initSelect() {
this.userDropdown = new UsersSelect(null, this.$refs.dropdown, {
......@@ -87,9 +90,6 @@ export default {
this.board.assignee = assignee;
},
},
mounted() {
this.initSelect();
},
};
</script>
......@@ -161,27 +161,33 @@ export default {
aria-hidden="true"
class="fa fa-chevron-down"
data-hidden="true"
/>
>
</i>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-user dropdown-menu-selectable dropdown-menu-author">
<div
class="dropdown-menu dropdown-select dropdown-menu-paging
dropdown-menu-user dropdown-menu-selectable dropdown-menu-author"
>
<div class="dropdown-input">
<input
autocomplete="off"
class="dropdown-input-field"
placeholder="Search"
type="search"
>
/>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
data-hidden="true"
/>
>
</i>
<i
aria-hidden="true"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
data-hidden="true"
role="button"
/>
>
</i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
......
<script>
/* eslint-disable vue/require-default-prop */
import './issue_card_inner';
import eventHub from '../eventhub';
......@@ -10,13 +11,33 @@ export default {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
props: {
list: Object,
issue: Object,
issueLinkBase: String,
disabled: Boolean,
index: Number,
rootPath: String,
groupId: Number,
list: {
type: Object,
default: () => ({}),
},
issue: {
type: Object,
default: () => ({}),
},
issueLinkBase: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
index: {
type: Number,
default: 0,
},
rootPath: {
type: String,
default: '',
},
groupId: {
type: Number,
},
},
data() {
return {
......@@ -55,8 +76,13 @@ export default {
</script>
<template>
<li class="card"
:class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
<li
class="card"
:class="{
'user-can-drag': !disabled && issue.id,
'is-disabled': disabled || !issue.id,
'is-active': issueDetailVisible
}"
:index="index"
:data-issue-id="issue.id"
@mousedown="mouseDown"
......@@ -68,6 +94,7 @@ export default {
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
:update-filters="true" />
:update-filters="true"
/>
</li>
</template>
......@@ -24,6 +24,13 @@ const boardDefaults = {
};
export default {
components: {
AssigneeSelect,
BoardLabelsSelect,
BoardMilestoneSelect,
BoardWeightSelect,
modal,
},
props: {
canAdminBoard: {
type: Boolean,
......@@ -55,6 +62,7 @@ export default {
weights: {
type: String,
required: false,
default: '',
},
},
data() {
......@@ -69,13 +77,6 @@ export default {
isLoading: false,
};
},
components: {
AssigneeSelect,
BoardLabelsSelect,
BoardMilestoneSelect,
BoardWeightSelect,
modal,
},
computed: {
isNewForm() {
return this.currentPage === 'new';
......@@ -132,6 +133,12 @@ export default {
return this.isLoading || this.board.name.length === 0;
},
},
mounted() {
this.resetFormState();
if (this.$refs.name) {
this.$refs.name.focus();
}
},
methods: {
submit() {
if (this.board.name.length === 0) return;
......@@ -169,12 +176,6 @@ export default {
}
},
},
mounted() {
this.resetFormState();
if (this.$refs.name) {
this.$refs.name.focus();
}
},
};
</script>
......@@ -217,7 +218,7 @@ export default {
v-model="board.name"
@keyup.enter="submit"
placeholder="Enter board name"
>
/>
</div>
<div v-if="scopedIssueBoardFeatureEnabled">
<div
......
......@@ -197,7 +197,7 @@ export default {
<li
class="board-list-count text-center"
v-if="showCount"
data-id="-1">
data-issue-id="-1">
<loading-icon
v-show="list.loadingMore"
......
<script>
/* global ListLabel */
/* global ListLabel */
import LabelsSelect from '~/labels_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import LabelsSelect from '~/labels_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
export default {
export default {
components: {
loadingIcon,
},
props: {
board: {
type: Object,
......@@ -20,9 +23,6 @@ export default {
default: false,
},
},
components: {
loadingIcon,
},
computed: {
labelIds() {
return this.board.labels.map(label => label.id);
......@@ -60,7 +60,7 @@ export default {
}
},
},
};
};
</script>
<template>
......@@ -106,12 +106,13 @@ export default {
v-for="labelId in labelIds"
:key="labelId"
:value="labelId"
>
/>
<div class="dropdown">
<button
ref="dropdownButton"
:data-labels="labelsPath"
class="dropdown-menu-toggle wide js-label-select js-multiselect js-extra-options js-board-config-modal"
class="dropdown-menu-toggle wide js-label-select
js-multiselect js-extra-options js-board-config-modal"
data-field-name="label_id[]"
:data-show-any="true"
data-toggle="dropdown"
......@@ -124,27 +125,33 @@ export default {
aria-hidden="true"
class="fa fa-chevron-down"
data-hidden="true"
/>
>
</i>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
<div
class="dropdown-menu dropdown-select
dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"
>
<div class="dropdown-input">
<input
autocomplete="off"
class="dropdown-input-field"
placeholder="Search"
type="search"
>
/>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
data-hidden="true"
/>
>
</i>
<i
aria-hidden="true"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
data-hidden="true"
role="button"
/>
>
</i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
......
<script>
/* global BoardService */
/* global BoardService */
import MilestoneSelect from '~/milestone_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import MilestoneSelect from '~/milestone_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
const ANY_MILESTONE = 'Any Milestone';
const NO_MILESTONE = 'No Milestone';
const ANY_MILESTONE = 'Any Milestone';
const NO_MILESTONE = 'No Milestone';
export default {
export default {
components: {
loadingIcon,
},
props: {
board: {
type: Object,
......@@ -23,9 +26,7 @@ export default {
default: false,
},
},
components: {
loadingIcon,
},
computed: {
milestoneTitle() {
if (this.noMilestone) return NO_MILESTONE;
......@@ -45,6 +46,11 @@ export default {
return this.board.milestone ? this.board.milestone.name : '';
},
},
mounted() {
this.milestoneDropdown = new MilestoneSelect(null, this.$refs.dropdownButton, {
handleClick: this.selectMilestone,
});
},
methods: {
selectMilestone(milestone) {
let id = milestone.id;
......@@ -61,12 +67,7 @@ export default {
};
},
},
mounted() {
this.milestoneDropdown = new MilestoneSelect(null, this.$refs.dropdownButton, {
handleClick: this.selectMilestone,
});
},
};
};
</script>
<template>
......@@ -95,7 +96,7 @@ export default {
:value="milestoneId"
name="milestone_id"
type="hidden"
>
/>
<div class="dropdown">
<button
ref="dropdownButton"
......@@ -115,7 +116,8 @@ export default {
aria-hidden="true"
data-hidden="true"
class="fa fa-chevron-down"
/>
>
</i>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div
......@@ -126,20 +128,23 @@ export default {
class="dropdown-input-field"
placeholder="Search milestones"
autocomplete="off"
>
/>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search"
/>
>
</i>
<i
role="button"
aria-hidden="true"
data-hidden="true"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
/>
>
</i>
</div>
<div class="dropdown-content">
</div>
<div class="dropdown-content" />
<div class="dropdown-loading">
<loading-icon />
</div>
......
<template>
<div>
<label class="label-light prepend-top-10">
Project
</label>
<div ref="projectsDropdown" class="dropdown">
<button
class="dropdown-menu-toggle wide"
type="button"
data-toggle="dropdown"
aria-expanded="false">
{{ selectedProjectName }}
<i class="fa fa-chevron-down" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">
<span>Projects</span>
<button aria-label="Close" type="button" class="dropdown-title-button dropdown-menu-close">
<i aria-hidden="true" data-hidden="true" class="fa fa-times dropdown-menu-close-icon"></i>
</button>
</div>
<div class="dropdown-input">
<input
class="dropdown-input-field"
type="search"
placeholder="Search projects">
<i aria-hidden="true" data-hidden="true" class="fa fa-search dropdown-input-search"></i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
</div>
</template>
<script>
/* global ListIssue */
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
/* global ListIssue */
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
export default {
export default {
name: 'BoardProjectSelect',
components: {
loadingIcon,
},
props: {
groupId: {
type: Number,
......@@ -56,9 +23,6 @@ export default {
selectedProject: {},
};
},
components: {
loadingIcon,
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
......@@ -99,5 +63,65 @@ export default {
text: project => project.name,
});
},
};
};
</script>
<template>
<div>
<label class="label-light prepend-top-10">
Project
</label>
<div
ref="projectsDropdown"
class="dropdown"
>
<button
class="dropdown-menu-toggle wide"
type="button"
data-toggle="dropdown"
aria-expanded="false"
>
{{ selectedProjectName }}
<i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">
<span>Projects</span>
<button
aria-label="Close"
type="button"
class="dropdown-title-button dropdown-menu-close"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-times dropdown-menu-close-icon"
>
</i>
</button>
</div>
<div class="dropdown-input">
<input
class="dropdown-input-field"
type="search"
placeholder="Search projects"
/>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search"
>
</i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
</div>
</template>
<script>
/* global BoardService */
/* eslint-disable vue/require-default-prop */
/* global BoardService */
import WeightSelect from 'ee/weight_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import WeightSelect from 'ee/weight_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
const ANY_WEIGHT = 'Any Weight';
const NO_WEIGHT = 'No Weight';
const ANY_WEIGHT = 'Any Weight';
const NO_WEIGHT = 'No Weight';
export default {
export default {
components: {
loadingIcon,
},
props: {
board: {
type: Object,
......@@ -32,9 +36,6 @@ export default {
fieldName: 'weight',
};
},
components: {
loadingIcon,
},
computed: {
valueClass() {
if (this.valueText === ANY_WEIGHT) {
......@@ -48,6 +49,13 @@ export default {
return ANY_WEIGHT;
},
},
mounted() {
this.weightDropdown = new WeightSelect(this.$refs.dropdownButton, {
handleClick: this.selectWeight,
selected: this.value,
fieldName: this.fieldName,
});
},
methods: {
selectWeight(weight) {
this.board.weight = this.weightInt(weight);
......@@ -62,14 +70,7 @@ export default {
return -1;
},
},
mounted() {
this.weightDropdown = new WeightSelect(this.$refs.dropdownButton, {
handleClick: this.selectWeight,
selected: this.value,
fieldName: this.fieldName,
});
},
};
};
</script>
<template>
......@@ -96,9 +97,9 @@ export default {
>
<input
type="hidden"
:name="this.fieldName"
:name="fieldName"
/>
<div class="dropdown ">
<div class="dropdown">
<button
ref="dropdownButton"
class="dropdown-menu-toggle js-weight-select wide"
......@@ -113,7 +114,8 @@ export default {
aria-hidden="true"
data-hidden="true"
class="fa fa-chevron-down"
/>
>
</i>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-weight">
<div class="dropdown-content ">
......
<script>
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
/* eslint-disable vue/require-default-prop */
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
APPLICATION_NOT_INSTALLABLE,
APPLICATION_SCHEDULED,
APPLICATION_INSTALLABLE,
......@@ -12,9 +13,12 @@ import {
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
} from '../constants';
} from '../constants';
export default {
export default {
components: {
loadingButton,
},
props: {
id: {
type: String,
......@@ -49,9 +53,6 @@ export default {
required: false,
},
},
components: {
loadingButton,
},
computed: {
rowJsClass() {
return `js-cluster-application-row-${this.id}`;
......@@ -66,7 +67,8 @@ export default {
// Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) ||
return (this.status !== APPLICATION_INSTALLABLE &&
this.status !== APPLICATION_ERROR) ||
this.requestStatus === REQUEST_LOADING ||
this.requestStatus === REQUEST_SUCCESS;
},
......@@ -78,7 +80,8 @@ export default {
this.status === APPLICATION_ERROR
) {
label = s__('ClusterIntegration|Install');
} else if (this.status === APPLICATION_SCHEDULED || this.status === APPLICATION_INSTALLING) {
} else if (this.status === APPLICATION_SCHEDULED ||
this.status === APPLICATION_INSTALLING) {
label = s__('ClusterIntegration|Installing');
} else if (this.status === APPLICATION_INSTALLED) {
label = s__('ClusterIntegration|Installed');
......@@ -87,7 +90,8 @@ export default {
return label;
},
hasError() {
return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE;
return this.status === APPLICATION_ERROR ||
this.requestStatus === REQUEST_FAILURE;
},
generalErrorDescription() {
return sprintf(
......@@ -102,7 +106,7 @@ export default {
eventHub.$emit('installApplication', this.id);
},
},
};
};
</script>
<template>
......
<script>
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
export default {
export default {
components: {
applicationRow,
},
props: {
applications: {
type: Object,
......@@ -13,15 +16,15 @@ export default {
helpPath: {
type: String,
required: false,
default: '',
},
},
components: {
applicationRow,
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), {
_.escape(s__(`ClusterIntegration|Install applications on your cluster.
Read more about %{helpLink}`)),
{
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
......@@ -43,7 +46,10 @@ export default {
));
const extraCostParagraph = sprintf(
_.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), {
_.escape(s__(`ClusterIntegration|%{boldNotice} This will add some
extra resources like a load balancer,
which incur additional costs. See %{pricingLink}`)),
{
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GKE pricing'))}
......@@ -69,8 +75,11 @@ export default {
},
prometheusDescription() {
return sprintf(
_.escape(s__('ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications.')), {
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html", target="_blank" rel="noopener noreferrer">
_.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`)),
{
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|Gitlab Integration'))}
</a>`,
},
......@@ -78,7 +87,7 @@ export default {
);
},
},
};
};
</script>
<template>
......@@ -126,7 +135,10 @@ export default {
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
/>
<!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests -->
<!--
NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests
-->
<!-- Add GitLab Runner row, all other plumbing is complete -->
</div>
</div>
......
......@@ -94,7 +94,7 @@ export default class ImageFile {
});
return [maxWidth, maxHeight];
}
// eslint-disable-next-line
views = {
'two-up': function() {
return $('.two-up.view .wrap', this.file).each((function(_this) {
......
......@@ -4,6 +4,9 @@
import pipelinesMixin from '../../pipelines/mixins/pipelines';
export default {
mixins: [
pipelinesMixin,
],
props: {
endpoint: {
type: String,
......@@ -31,9 +34,6 @@
default: 'child',
},
},
mixins: [
pipelinesMixin,
],
data() {
const store = new PipelineStore();
......@@ -110,7 +110,8 @@
<div
class="table-holder"
v-if="shouldRenderTable">
v-if="shouldRenderTable"
>
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
......
......@@ -26,28 +26,34 @@
class="js-ca-dismiss-button dismiss-button"
type="button"
:aria-label="__('Dismiss Cycle Analytics introduction box')"
@click="dismissOverviewDialog">
@click="dismissOverviewDialog"
>
<i
class="fa fa-times"
aria-hidden="true">
</i>
</button>
<div class="svg-container" v-html="iconCycleAnalyticsSplash">
<div
class="svg-container"
v-html="iconCycleAnalyticsSplash"
>
</div>
<div class="inner-content">
<h4>
{{__('Introducing Cycle Analytics')}}
{{ __('Introducing Cycle Analytics') }}
</h4>
<p>
{{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }}
{{ __(`Cycle Analytics gives an overview
of how much time it takes to go from idea to production in your project.`) }}
</p>
<p>
<a
:href="documentationLink"
target="_blank"
rel="nofollow"
class="btn">
{{__('Read more')}}
class="btn"
>
{{ __('Read more') }}
</a>
</p>
</div>
......
......@@ -2,25 +2,34 @@
import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
props: {
count: {
type: Number,
required: true,
},
},
directives: {
tooltip,
},
};
</script>
<template>
<span v-if="count === 50" class="events-info pull-right">
<span
v-if="count === 50"
class="events-info pull-right"
>
<i
class="fa fa-warning"
v-tooltip
aria-hidden="true"
:title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
data-placement="top"></i>
:title="n__(
'Limited to showing %d event at most',
'Limited to showing %d events at most',
50
)"
data-placement="top"
>
</i>
{{ n__('Showing %d event', 'Showing %d events', 50) }}
</span>
</template>
......@@ -4,15 +4,21 @@
import totalTime from './total_time_component.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
limitWarning,
totalTime,
},
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
};
</script>
<template>
......@@ -22,28 +28,47 @@
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<li
v-for="(mergeRequest, i) in items"
:key="i"
class="stage-event-item"
>
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
<a
:href="mergeRequest.url"
class="issue-link"
>
!{{ mergeRequest.iid }}
</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
<a
:href="mergeRequest.url"
class="issue-date"
>
{{ mergeRequest.createdAt }}
</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
<a
:href="mergeRequest.author.webUrl"
class="issue-author-link"
>
{{ mergeRequest.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
<total-time :time="mergeRequest.totalTime" />
</div>
</li>
</ul>
......
......@@ -4,15 +4,21 @@
import totalTime from './total_time_component.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
limitWarning,
totalTime,
},
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
};
</script>
<template>
......@@ -25,30 +31,47 @@
<li
v-for="(issue, i) in items"
:key="i"
class="stage-event-item">
class="stage-event-item"
>
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
<a
class="issue-title"
:href="issue.url"
>
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
<a
:href="issue.url"
class="issue-link"
>
#{{ issue.iid }}
</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
<a
:href="issue.url"
class="issue-date"
>
{{ issue.createdAt }}
</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
<a
:href="issue.author.webUrl"
class="issue-author-link"
>
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"/>
<total-time :time="issue.totalTime" />
</div>
</li>
</ul>
......
......@@ -5,15 +5,21 @@
import totalTime from './total_time_component.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
totalTime,
limitWarning,
},
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
computed: {
iconCommit() {
return iconCommit;
......@@ -31,10 +37,11 @@
<li
v-for="(commit, i) in items"
:key="i"
class="stage-event-item">
class="stage-event-item"
>
<div class="item-details item-conmmit-component">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl"/>
<user-avatar-image :img-src="commit.author.avatarUrl" />
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
......@@ -42,10 +49,22 @@
</h5>
<span>
{{ s__('FirstPushedBy|First') }}
<span class="commit-icon" v-html="iconCommit"></span>
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
<span
class="commit-icon"
v-html="iconCommit"
>
</span>
<a
:href="commit.commitUrl"
class="commit-hash-link commit-sha"
>
{{ commit.shortSha }}
</a>
{{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
<a
:href="commit.author.webUrl"
class="commit-author-link"
>
{{ commit.author.name }}
</a>
</span>
......
......@@ -5,16 +5,22 @@
import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
totalTime,
limitWarning,
icon,
},
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
};
</script>
<template>
......@@ -27,43 +33,64 @@
<li
v-for="(mergeRequest, i) in items"
:key="i"
class="stage-event-item">
class="stage-event-item"
>
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
<a
:href="mergeRequest.url"
class="issue-link"
>
!{{ mergeRequest.iid }}
</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
<a
:href="mergeRequest.url"
class="issue-date"
>{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
<a
:href="mergeRequest.author.webUrl"
class="issue-author-link"
>{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
<i
class="fa fa-ban"
aria-hidden="true"
>
</i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<span
class="merge-request-branch"
v-if="mergeRequest.branch"
>
<icon
name="fork"
:size="16">
</icon>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
:size="16"
/>
<a :href="mergeRequest.branch.url">
{{ mergeRequest.branch.name }}
</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"/>
<total-time :time="mergeRequest.totalTime" />
</div>
</li>
</ul>
......
......@@ -6,16 +6,22 @@
import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
totalTime,
limitWarning,
icon,
},
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
computed: {
iconBranch() {
return iconBranch;
......@@ -33,30 +39,57 @@
<li
v-for="(build, i) in items"
class="stage-event-item item-build-component"
:key="i">
:key="i"
>
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<a
:href="build.url"
class="pipeline-id"
>
#{{ build.id }}
</a>
<icon
name="fork"
:size="16">
</icon>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
:size="16"
/>
<a
:href="build.branch.url"
class="ref-name"
>
{{ build.branch.name }}
</a>
<span
class="icon-branch"
v-html="iconBranch">
</span>
<a
:href="build.commitUrl"
class="commit-sha"
>
{{ build.shortSha }}
</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
<a
:href="build.url"
class="build-date"
>
{{ build.date }}
</a>
{{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link">
<a
:href="build.author.webUrl"
class="issue-author-link"
>
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"/>
<total-time :time="build.totalTime" />
</div>
</li>
</ul>
......
......@@ -6,15 +6,21 @@
import icon from '../../vue_shared/components/icon.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
totalTime,
limitWarning,
icon,
},
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
computed: {
iconBuildStatus() {
return iconBuildStatus;
......@@ -35,29 +41,61 @@
<li
v-for="(build, i) in items"
:key="i"
class="stage-event-item item-build-component">
class="stage-event-item item-build-component"
>
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status" v-html="iconBuildStatus"></span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
<span
class="icon-build-status"
v-html="iconBuildStatus"
>
</span>
<a
:href="build.url"
class="item-build-name"
>
{{ build.name }}
</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<a
:href="build.url"
class="pipeline-id"
>
#{{ build.id }}
</a>
<icon
name="fork"
:size="16">
</icon>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
:size="16"
/>
<a
:href="build.branch.url"
class="ref-name"
>
{{ build.branch.name }}
</a>
<span
class="icon-branch"
v-html="iconBranch"
>
</span>
<a
:href="build.commitUrl"
class="commit-sha"
>
{{ build.shortSha }}
</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
<a
:href="build.url"
class="issue-date"
>
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"/>
<total-time :time="build.totalTime" />
</div>
</li>
</ul>
......
......@@ -17,10 +17,30 @@
<template>
<span class="total-time">
<template v-if="hasData">
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && hasData === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
<template v-if="time.days">
{{ time.days }}
<span>
{{ n__('day', 'days', time.days) }}
</span>
</template>
<template v-if="time.hours">
{{ time.hours }}
<span>
{{ n__('Time|hr', 'Time|hrs', time.hours) }}
</span>
</template>
<template v-if="time.mins && !time.days">
{{ time.mins }}
<span>
{{ n__('Time|min', 'Time|mins', time.mins) }}
</span>
</template>
<template v-if="time.seconds && hasData === 1 || time.seconds === 0">
{{ time.seconds }}
<span>
{{ s__('Time|s') }}
</span>
</template>
</template>
<template v-else>
--
......
......@@ -20,6 +20,16 @@ $(() => {
gl.cycleAnalyticsApp = new Vue({
el: '#cycle-analytics',
name: 'CycleAnalytics',
components: {
banner,
'stage-issue-component': stageComponent,
'stage-plan-component': stagePlanComponent,
'stage-code-component': stageCodeComponent,
'stage-test-component': stageTestComponent,
'stage-review-component': stageReviewComponent,
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
},
data() {
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsService = new CycleAnalyticsService({
......@@ -43,16 +53,6 @@ $(() => {
return this.store.currentActiveStage();
},
},
components: {
banner,
'stage-issue-component': stageComponent,
'stage-plan-component': stagePlanComponent,
'stage-code-component': stageCodeComponent,
'stage-test-component': stageTestComponent,
'stage-review-component': stageReviewComponent,
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
},
created() {
this.fetchCycleAnalyticsData();
},
......
......@@ -3,10 +3,8 @@
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
data() {
return {
isLoading: false,
};
components: {
loadingIcon,
},
props: {
deployKey: {
......@@ -23,11 +21,16 @@
default: 'btn-default',
},
},
components: {
loadingIcon,
data() {
return {
isLoading: false,
};
},
computed: {
text() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
},
},
methods: {
doAction() {
this.isLoading = true;
......@@ -37,11 +40,6 @@
});
},
},
computed: {
text() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
},
},
};
</script>
......
......@@ -7,11 +7,9 @@
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
data() {
return {
isLoading: false,
store: new DeployKeysStore(),
};
components: {
keysPanel,
loadingIcon,
},
props: {
endpoint: {
......@@ -19,6 +17,12 @@
required: true,
},
},
data() {
return {
isLoading: false,
store: new DeployKeysStore(),
};
},
computed: {
hasKeys() {
return Object.keys(this.keys).length;
......@@ -27,9 +31,20 @@
return this.store.keys;
},
},
components: {
keysPanel,
loadingIcon,
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);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
},
methods: {
fetchKeys() {
......@@ -59,21 +74,6 @@
}
},
},
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);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
},
};
</script>
......
......@@ -3,6 +3,9 @@
import { getTimeago } from '../../lib/utils/datetime_utility';
export default {
components: {
actionBtn,
},
props: {
deployKey: {
type: Object,
......@@ -17,9 +20,6 @@
required: true,
},
},
components: {
actionBtn,
},
computed: {
timeagoDate() {
return getTimeago().format(this.deployKey.created_at);
......@@ -61,9 +61,10 @@
</div>
<div class="deploy-key-content prepend-left-default deploy-key-projects">
<a
v-for="project in deployKey.projects"
v-for="(project, i) in deployKey.projects"
class="label deploy-project-label"
:href="project.full_path"
:key="i"
>
{{ project.full_name }}
</a>
......
......@@ -2,6 +2,9 @@
import key from './key.vue';
export default {
components: {
key,
},
props: {
title: {
type: String,
......@@ -25,9 +28,6 @@
required: true,
},
},
components: {
key,
},
};
</script>
......@@ -37,12 +37,14 @@
{{ title }}
({{ keys.length }})
</h5>
<ul class="well-list"
<ul
class="well-list"
v-if="keys.length"
>
<li
v-for="deployKey in keys"
:key="deployKey.id">
:key="deployKey.id"
>
<key
:deploy-key="deployKey"
:store="store"
......
......@@ -3,14 +3,14 @@ import deployKeysApp from './components/app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: document.getElementById('js-deploy-keys'),
components: {
deployKeysApp,
},
data() {
return {
endpoint: this.$options.el.dataset.endpoint,
};
},
components: {
deployKeysApp,
},
render(createElement) {
return createElement('deploy-keys-app', {
props: {
......
......@@ -13,7 +13,6 @@ import groupAvatar from './group_avatar';
import GroupLabelSubscription from './group_label_subscription';
import LineHighlighter from './line_highlighter';
import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor';
import groupsSelect from './groups_select';
import Search from './search';
import initAdmin from './admin';
......@@ -215,16 +214,26 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
initIssuableSidebar();
break;
case 'dashboard:milestones:index':
projectSelect();
import('./pages/dashboard/milestones/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:milestones:show':
new UserCallout();
case 'groups:milestones:show':
case 'dashboard:milestones:show':
new Milestone();
new Sidebar();
break;
case 'dashboard:milestones:show':
import('./pages/dashboard/milestones/show')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:issues':
import('./pages/dashboard/issues')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:merge_requests':
projectSelect();
initLegacyFilters();
......@@ -240,6 +249,12 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
case 'dashboard:todos:index':
import('./pages/dashboard/todos/index').then(callDefault).catch(fail);
break;
case 'dashboard:projects:index':
case 'dashboard:projects:starred':
import('./pages/dashboard/projects')
.then(callDefault)
.catch(fail);
break;
case 'explore:projects:index':
case 'explore:projects:trending':
case 'explore:projects:starred':
......@@ -600,22 +615,19 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
break;
case 'ci:lints:create':
case 'ci:lints:show':
new CILintEditor();
import('./pages/ci/lints').then(m => m.default()).catch(fail);
break;
case 'users:show':
import('./pages/users/show').then(callDefault).catch(fail);
break;
case 'admin:conversational_development_index:show':
new UserCallout();
import('./pages/admin/conversational_development_index/show').then(m => m.default()).catch(fail);
break;
case 'snippets:show':
new LineHighlighter();
new BlobViewer();
initNotes();
new ZenMode();
import('./pages/snippets/show').then(m => m.default()).catch(fail);
break;
case 'import:fogbugz:new_user_map':
new UsersSelect();
import('./pages/import/fogbugz/new_user_map').then(m => m.default()).catch(fail);
break;
case 'profiles:personal_access_tokens:index':
case 'admin:impersonation_tokens:index':
......
......@@ -4,6 +4,11 @@
import environmentTable from '../components/environments_table.vue';
export default {
components: {
environmentTable,
loadingIcon,
tablePagination,
},
props: {
isLoading: {
type: Boolean,
......@@ -26,12 +31,6 @@
required: true,
},
},
components: {
environmentTable,
loadingIcon,
tablePagination,
},
methods: {
onChangePage(page) {
this.$emit('onChangePage', page);
......@@ -64,7 +63,7 @@
<table-pagination
v-if="pagination && pagination.totalPages > 1"
:change="onChangePage"
:pageInfo="pagination"
:page-info="pagination"
/>
</div>
</div>
......
......@@ -61,20 +61,21 @@
<section class="deploy-board-information">
<span>
<span class="percentage">{{deployBoardData.completion}}%</span>
<span class="percentage">{{ deployBoardData.completion }}%</span>
<span class="text">Complete</span>
</span>
</section>
<section class="deploy-board-instances">
<p class="text">{{instanceTitle}}</p>
<p class="text">{{ instanceTitle }}</p>
<div class="deploy-board-instances-container">
<template v-for="instance in deployBoardData.instances">
<template v-for="(instance, i) in deployBoardData.instances">
<instance-component
:status="instance.status"
:tooltip-text="instance.tooltip"
:stable="instance.stable"
:key="i"
/>
</template>
</div>
......@@ -82,7 +83,8 @@
<section
class="deploy-board-actions"
v-if="deployBoardData.rollback_url || deployBoardData.abort_url">
v-if="deployBoardData.rollback_url || deployBoardData.abort_url"
>
<a
class="btn"
data-method="post"
......@@ -112,10 +114,11 @@
<section class="deploy-board-empty-state-text">
<span class="title">Kubernetes deployment not found</span>
<span>
To see deployment progress for your environments, make sure your deployments are in Kubernetes namespace
<code>{{projectName}}</code> and labeled with <code>app=$CI_ENVIRONMENT_SLUG</code>.
To see deployment progress for your environments,
make sure your deployments are in Kubernetes namespace
<code>{{ projectName }}</code> and labeled with <code>app=$CI_ENVIRONMENT_SLUG</code>.
</span>
</section>
</div>
</div>
</script>
</template>
<script>
/**
/**
* An instance in deploy board is represented by a square in this mockup:
* https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png
*
......@@ -12,9 +12,13 @@
* this information in the tooltip and the colors.
* Mockup is https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1551#note_26595150
*/
import tooltip from '../../vue_shared/directives/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
export default {
props: {
/**
* Represents the status of the pod. Each state is represented with a different
......@@ -41,10 +45,6 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
cssClass() {
let cssClassName = `deploy-board-instance-${this.status}`;
......@@ -56,7 +56,7 @@ export default {
return cssClassName;
},
},
};
};
</script>
<template>
<div
......@@ -64,6 +64,7 @@ export default {
class="deploy-board-instance"
:class="cssClass"
:data-title="tooltipText"
data-placement="top">
data-placement="top"
>
</div>
</template>
<script>
export default {
name: 'environmentsEmptyState',
name: 'EnvironmentsEmptyState',
props: {
newPath: {
type: String,
......@@ -21,21 +21,23 @@
<div class="blank-state-row">
<div class="blank-state-center">
<h2 class="blank-state-title js-blank-state-title">
{{s__("Environments|You don't have any environments right now.")}}
{{ s__("Environments|You don't have any environments right now.") }}
</h2>
<p class="blank-state-text">
{{s__("Environments|Environments are places where code gets deployed, such as staging or production.")}}
{{ s__(`Environments|Environments are places where
code gets deployed, such as staging or production.`) }}
<br />
<a :href="helpPath">
{{s__("Environments|Read more about environments")}}
{{ s__("Environments|Read more about environments") }}
</a>
</p>
<a
v-if="canCreateEnvironment"
:href="newPath"
class="btn btn-create js-new-environment-button">
{{s__("Environments|New environment")}}
class="btn btn-create js-new-environment-button"
>
{{ s__("Environments|New environment") }}
</a>
</div>
</div>
......
<script>
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
},
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
......@@ -21,6 +13,14 @@ export default {
loadingIcon,
},
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
playIconSvg,
......@@ -49,7 +49,7 @@ export default {
return !action.playable;
},
},
};
};
</script>
<template>
<div
......@@ -63,27 +63,33 @@ export default {
data-toggle="dropdown"
:title="title"
:aria-label="title"
:disabled="isLoading">
:disabled="isLoading"
>
<span>
<span v-html="playIconSvg"></span>
<i
class="fa fa-caret-down"
aria-hidden="true"/>
aria-hidden="true">
</i>
<loading-icon v-if="isLoading" />
</span>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<li
v-for="(action, i) in actions"
:key="i"
>
<button
type="button"
class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)"
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
:disabled="isActionDisabled(action)"
>
<span v-html="playIconSvg"></span>
<span>
{{action.name}}
{{ action.name }}
</span>
</button>
</li>
......
<script>
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
/**
/**
* Renders the external url link in environments table.
*/
export default {
export default {
directives: {
tooltip,
},
props: {
externalUrl: {
type: String,
......@@ -13,16 +17,12 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
title() {
return s__('Environments|Open');
},
},
};
};
</script>
<template>
<a
......@@ -33,9 +33,12 @@ export default {
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title"
:href="externalUrl">
:href="externalUrl"
>
<i
class="fa fa-external-link"
aria-hidden="true" />
aria-hidden="true"
>
</i>
</a>
</template>
<script>
import Timeago from 'timeago.js';
import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
const timeagoInstance = new Timeago();
export default {
import Timeago from 'timeago.js';
import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
const timeagoInstance = new Timeago();
export default {
components: {
userAvatarLink,
'commit-component': CommitComponent,
......@@ -415,7 +420,7 @@ export default {
eventHub.$emit('toggleDeployBoard', this.model);
},
},
};
};
</script>
<template>
<div
......@@ -425,12 +430,16 @@ export default {
'folder-row': model.isFolder,
}"
role="row">
<div class="table-section section-10" role="gridcell">
<div
class="table-section section-10"
role="gridcell"
>
<div
v-if="!model.isFolder"
class="table-mobile-header"
role="rowheader">
{{s__("Environments|Environment")}}
role="rowheader"
>
{{ s__("Environments|Environment") }}
</div>
<span
class="deploy-board-icon"
......@@ -453,7 +462,7 @@ export default {
v-if="!model.isFolder"
class="environment-name flex-truncate-parent table-mobile-content"
:href="environmentPath">
<span class="flex-truncate-child">{{model.name}}</span>
<span class="flex-truncate-child">{{ model.name }}</span>
</a>
<span
v-else
......@@ -465,32 +474,41 @@ export default {
<i
v-show="model.isOpen"
class="fa fa-caret-down"
aria-hidden="true" />
aria-hidden="true"
>
</i>
<i
v-show="!model.isOpen"
class="fa fa-caret-right"
aria-hidden="true"/>
aria-hidden="true"
>
</i>
</span>
<span class="folder-icon">
<i
class="fa fa-folder"
aria-hidden="true" />
aria-hidden="true"
>
</i>
</span>
<span>
{{model.folderName}}
{{ model.folderName }}
</span>
<span class="badge">
{{model.size}}
{{ model.size }}
</span>
</span>
</div>
<div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell">
<div
class="table-section section-10 deployment-column hidden-xs hidden-sm"
role="gridcell"
>
<span v-if="shouldRenderDeploymentID">
{{deploymentInternalId}}
{{ deploymentInternalId }}
</span>
<span v-if="!model.isFolder && deploymentHasUser">
......@@ -505,22 +523,29 @@ export default {
</span>
</div>
<div class="table-section section-15 hidden-xs hidden-sm" role="gridcell">
<div
class="table-section section-15 hidden-xs hidden-sm"
role="gridcell"
>
<a
v-if="shouldRenderBuildName"
class="build-link flex-truncate-parent"
:href="buildPath">
<span class="flex-truncate-child">{{buildName}}</span>
:href="buildPath"
>
<span class="flex-truncate-child">{{ buildName }}</span>
</a>
</div>
<div
v-if="!model.isFolder"
class="table-section section-25" role="gridcell">
class="table-section section-25"
role="gridcell"
>
<div
role="rowheader"
class="table-mobile-header">
{{s__("Environments|Commit")}}
class="table-mobile-header"
>
{{ s__("Environments|Commit") }}
</div>
<div
v-if="hasLastDeploymentKey"
......@@ -536,22 +561,24 @@ export default {
<div
v-if="!hasLastDeploymentKey"
class="commit-title table-mobile-content">
{{s__("Environments|No deployments yet")}}
{{ s__("Environments|No deployments yet") }}
</div>
</div>
<div
v-if="!model.isFolder"
class="table-section section-10" role="gridcell">
class="table-section section-10"
role="gridcell"
>
<div
role="rowheader"
class="table-mobile-header">
{{s__("Environments|Updated")}}
{{ s__("Environments|Updated") }}
</div>
<span
v-if="canShowDate"
class="environment-created-date-timeago table-mobile-content">
{{createdDate}}
{{ createdDate }}
</span>
</div>
......
<script>
/**
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import tooltip from '../../vue_shared/directives/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
export default {
directives: {
tooltip,
},
props: {
monitoringUrl: {
type: String,
......@@ -12,16 +15,12 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
title() {
return 'Monitoring';
},
},
};
};
</script>
<template>
<a
......@@ -31,10 +30,12 @@ export default {
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
:aria-label="title">
:aria-label="title"
>
<i
class="fa fa-area-chart"
aria-hidden="true"
/>
>
</i>
</a>
</template>
<script>
/**
/**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`.
*
* Makes a post request when the button is clicked.
*/
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
export default {
components: {
loadingIcon,
},
props: {
retryUrl: {
type: String,
......@@ -21,10 +24,6 @@ export default {
},
},
components: {
loadingIcon,
},
data() {
return {
isLoading: false,
......@@ -38,20 +37,21 @@ export default {
eventHub.$emit('postAction', this.retryUrl);
},
},
};
};
</script>
<template>
<button
type="button"
class="btn hidden-xs hidden-sm"
@click="onClick"
:disabled="isLoading">
:disabled="isLoading"
>
<span v-if="isLastDeployment">
{{s__("Environments|Re-deploy")}}
{{ s__("Environments|Re-deploy") }}
</span>
<span v-else>
{{s__("Environments|Rollback")}}
{{ s__("Environments|Rollback") }}
</span>
<loading-icon v-if="isLoading" />
......
<script>
/**
/**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
loadingIcon,
},
directives: {
tooltip,
},
export default {
props: {
stopUrl: {
type: String,
......@@ -15,20 +22,12 @@ export default {
},
},
directives: {
tooltip,
},
data() {
return {
isLoading: false,
};
},
components: {
loadingIcon,
},
computed: {
title() {
return 'Stop';
......@@ -47,7 +46,7 @@ export default {
}
},
},
};
};
</script>
<template>
<button
......@@ -58,10 +57,13 @@ export default {
@click="onClick"
:disabled="isLoading"
:title="title"
:aria-label="title">
:aria-label="title"
>
<i
class="fa fa-stop stop-env-icon"
aria-hidden="true" />
aria-hidden="true"
>
</i>
<loading-icon v-if="isLoading" />
</button>
</template>
<script>
/**
/**
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
import terminalIconSvg from 'icons/_icon_terminal.svg';
import tooltip from '../../vue_shared/directives/tooltip';
import terminalIconSvg from 'icons/_icon_terminal.svg';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
export default {
directives: {
tooltip,
},
props: {
terminalPath: {
type: String,
......@@ -14,11 +17,6 @@ export default {
default: '',
},
},
directives: {
tooltip,
},
data() {
return {
terminalIconSvg,
......@@ -30,7 +28,7 @@ export default {
return 'Terminal';
},
},
};
};
</script>
<template>
<a
......@@ -40,6 +38,7 @@ export default {
:title="title"
:aria-label="title"
:href="terminalPath"
v-html="terminalIconSvg">
v-html="terminalIconSvg"
>
</a>
</template>
......@@ -7,6 +7,15 @@
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
components: {
emptyState,
},
mixins: [
CIPaginationMixin,
environmentsMixin,
],
props: {
endpoint: {
type: String,
......@@ -37,14 +46,6 @@
required: true,
},
},
components: {
emptyState,
},
mixins: [
CIPaginationMixin,
environmentsMixin,
],
created() {
eventHub.$on('toggleFolder', this.toggleFolder);
......@@ -109,11 +110,13 @@
<div
v-if="canCreateEnvironment && !isLoading"
class="nav-controls">
class="nav-controls"
>
<a
:href="newEnvironmentPath"
class="btn btn-create">
{{s__("Environments|New environment")}}
class="btn btn-create"
>
{{ s__("Environments|New environment") }}
</a>
</div>
</div>
......
......@@ -37,39 +37,72 @@ export default {
folderUrl(model) {
return `${window.location.pathname}/folders/${model.folderName}`;
},
shouldRenderFolderContent(env) {
return env.isFolder &&
env.isOpen &&
env.children &&
env.children.length > 0;
},
},
};
</script>
<template>
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="columnheader">
{{s__("Environments|Environment")}}
<div
class="ci-table"
role="grid"
>
<div
class="gl-responsive-table-row table-row-header"
role="row"
>
<div
class="table-section section-10 environments-name"
role="columnheader"
>
{{ s__("Environments|Environment") }}
</div>
<div class="table-section section-10 environments-deploy" role="columnheader">
{{s__("Environments|Deployment")}}
<div
class="table-section section-10 environments-deploy"
role="columnheader"
>
{{ s__("Environments|Deployment") }}
</div>
<div class="table-section section-15 environments-build" role="columnheader">
{{s__("Environments|Job")}}
<div
class="table-section section-15 environments-build"
role="columnheader"
>
{{ s__("Environments|Job") }}
</div>
<div class="table-section section-25 environments-commit" role="columnheader">
{{s__("Environments|Commit")}}
<div
class="table-section section-25 environments-commit"
role="columnheader"
>
{{ s__("Environments|Commit") }}
</div>
<div class="table-section section-10 environments-date" role="columnheader">
{{s__("Environments|Updated")}}
<div
class="table-section section-10 environments-date"
role="columnheader"
>
{{ s__("Environments|Updated") }}
</div>
</div>
<template
v-for="model in environments"
v-bind:model="model">
v-for="(model, i) in environments"
:model="model"
>
<div
is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:key="i"
/>
<div v-if="model.hasDeployBoard && model.isDeployBoardVisible" class="js-deploy-board-row">
<div
v-if="model.hasDeployBoard && model.isDeployBoardVisible"
class="js-deploy-board-row"
:key="i"
>
<div class="deploy-board-container">
<deploy-board
:deploy-board-data="model.deployBoardData"
......@@ -79,26 +112,33 @@ export default {
</div>
</div>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<div v-if="model.isLoadingFolderContent">
<template
v-if="shouldRenderFolderContent(model)"
>
<div
v-if="model.isLoadingFolderContent"
:key="i"
>
<loading-icon size="2" />
</div>
<template v-else>
<div
is="environment-item"
v-for="children in model.children"
v-for="(children, index) in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:key="index"
/>
<div>
<div :key="i">
<div class="text-center prepend-top-10">
<a
:href="folderUrl(model)"
class="btn btn-default">
{{s__("Environments|Show all")}}
class="btn btn-default"
>
{{ s__("Environments|Show all") }}
</a>
</div>
</div>
......
......@@ -3,6 +3,10 @@
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
mixins: [
environmentsMixin,
CIPaginationMixin,
],
props: {
endpoint: {
type: String,
......@@ -26,11 +30,6 @@
},
},
mixins: [
environmentsMixin,
CIPaginationMixin,
],
methods: {
successCallback(resp) {
this.saveData(resp);
......@@ -42,10 +41,11 @@
<div :class="cssContainerClass">
<div
class="top-area"
v-if="!isLoading">
v-if="!isLoading"
>
<h4 class="js-folder-name environments-folder-name">
{{s__("Environments|Environments")}} / <b>{{folderName}}</b>
{{ s__("Environments|Environments") }} / <b>{{ folderName }}</b>
</h4>
<tabs
......
......@@ -32,6 +32,9 @@ class RecentSearchesRoot {
const state = this.store.state;
this.vm = new Vue({
el: this.wrapperElement,
components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent,
},
data() { return state; },
template: `
<recent-searches-dropdown-content
......@@ -40,9 +43,6 @@ class RecentSearchesRoot {
:allowed-keys="allowedKeys"
/>
`,
components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent,
},
});
}
......
......@@ -42,6 +42,26 @@ export default {
return this.store.getPaginationInfo();
},
},
created() {
this.searchEmptyMessage = this.hideProjects ?
COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleChildren', this.toggleChildren);
eventHub.$on('leaveGroup', this.leaveGroup);
eventHub.$on('updatePagination', this.updatePagination);
eventHub.$on('updateGroups', this.updateGroups);
},
mounted() {
this.fetchAllGroups();
},
beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage);
eventHub.$off('toggleChildren', this.toggleChildren);
eventHub.$off('leaveGroup', this.leaveGroup);
eventHub.$off('updatePagination', this.updatePagination);
eventHub.$off('updateGroups', this.updateGroups);
},
methods: {
fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
......@@ -152,26 +172,6 @@ export default {
}
},
},
created() {
this.searchEmptyMessage = this.hideProjects ?
COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleChildren', this.toggleChildren);
eventHub.$on('leaveGroup', this.leaveGroup);
eventHub.$on('updatePagination', this.updatePagination);
eventHub.$on('updateGroups', this.updateGroups);
},
mounted() {
this.fetchAllGroups();
},
beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage);
eventHub.$off('toggleChildren', this.toggleChildren);
eventHub.$off('leaveGroup', this.leaveGroup);
eventHub.$off('updatePagination', this.updatePagination);
eventHub.$off('updateGroups', this.updateGroups);
},
};
</script>
......
......@@ -20,7 +20,11 @@ export default {
return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT;
},
moreChildrenStats() {
return n__('One more item', '%d more items', this.parentGroup.childrenCount - this.parentGroup.children.length);
return n__(
'One more item',
'%d more items',
this.parentGroup.childrenCount - this.parentGroup.children.length,
);
},
},
};
......@@ -43,8 +47,9 @@ export default {
<i
class="fa fa-external-link"
aria-hidden="true"
/>
{{moreChildrenStats}}
>
</i>
{{ moreChildrenStats }}
</a>
</li>
</ul>
......
......@@ -88,7 +88,8 @@ export default {
:item="group"
/>
<div
class="folder-toggle-wrap">
class="folder-toggle-wrap"
>
<item-caret
:is-group-open="group.isOpen"
/>
......@@ -113,13 +114,14 @@ export default {
<identicon
v-else
size-class="s24"
:entity-id=group.id
:entity-id="group.id"
:entity-name="group.name"
/>
</a>
</div>
<div
class="title namespace-title">
class="title namespace-title"
>
<a
v-tooltip
:href="group.relativePath"
......@@ -135,7 +137,7 @@ export default {
v-if="group.permission"
class="user-access-role"
>
{{group.permission}}
{{ group.permission }}
</span>
</div>
<div
......
<script>
import tablePagination from '~/vue_shared/components/table_pagination.vue';
import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils';
import tablePagination from '~/vue_shared/components/table_pagination.vue';
import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils';
export default {
export default {
components: {
tablePagination,
},
......@@ -33,15 +33,16 @@ export default {
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
},
},
};
};
</script>
<template>
<div class="groups-list-tree-container">
<div
v-if="searchEmpty"
class="has-no-search-results">
{{searchEmptyMessage}}
class="has-no-search-results"
>
{{ searchEmptyMessage }}
</div>
<group-folder
v-if="!searchEmpty"
......@@ -50,7 +51,7 @@ export default {
<table-pagination
v-if="!searchEmpty"
:change="change"
:pageInfo="pageInfo"
:page-info="pageInfo"
/>
</div>
</template>
<script>
import { s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
import { s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
export default {
export default {
components: {
icon,
modal,
......@@ -50,7 +50,7 @@ export default {
eventHub.$emit('leaveGroup', this.group, this.parentGroup);
},
},
};
};
</script>
<template>
......
......@@ -2,6 +2,9 @@
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
isGroupOpen: {
type: Boolean,
......@@ -9,9 +12,6 @@ export default {
default: false,
},
},
components: {
icon,
},
computed: {
iconClass() {
return this.isGroupOpen ? 'angle-down' : 'angle-right';
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants';
import itemStatsValue from './item_stats_value.vue';
import icon from '~/vue_shared/components/icon.vue';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
ITEM_TYPE,
VISIBILITY_TYPE_ICON,
GROUP_VISIBILITY_TYPE,
PROJECT_VISIBILITY_TYPE,
} from '../constants';
import itemStatsValue from './item_stats_value.vue';
export default {
export default {
components: {
icon,
timeAgoTooltip,
......@@ -33,7 +38,7 @@ export default {
return this.item.type === ITEM_TYPE.GROUP;
},
},
};
};
</script>
<template>
......
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
export default {
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
title: {
type: String,
......@@ -35,18 +41,12 @@ export default {
default: '',
},
},
directives: {
tooltip,
},
components: {
icon,
},
computed: {
isValuePresent() {
return this.value !== '';
},
},
};
};
</script>
<template>
......@@ -57,12 +57,12 @@ export default {
:class="cssClass"
:title="title"
>
<icon :name="iconName"/>
<icon :name="iconName" />
<span
v-if="isValuePresent"
class="stat-value"
>
{{value}}
{{ value }}
</span>
</span>
</template>
......@@ -32,7 +32,6 @@
this.$emit('toggleCollapsed');
},
},
};
</script>
......
<script>
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue';
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue';
export default {
export default {
components: {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
repoPreview,
},
props: {
emptyStateSvgPath: {
type: String,
......@@ -25,15 +34,6 @@ export default {
'activeFile',
]),
},
components: {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
repoPreview,
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
......@@ -45,35 +45,38 @@ export default {
return returnValue;
};
},
};
};
</script>
<template>
<div
class="ide-view"
>
<ide-sidebar/>
<ide-sidebar />
<div
class="multi-file-edit-pane"
>
<template
v-if="activeFile">
v-if="activeFile"
>
<repo-tabs/>
<component
class="multi-file-edit-pane-content"
:is="currentBlobView"
/>
<repo-file-buttons/>
<repo-file-buttons />
<ide-status-bar
:file="selectedFile"/>
:file="selectedFile"
/>
</template>
<template
v-else>
v-else
>
<div class="ide-empty-state">
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath">
<img :src="emptyStateSvgPath" />
</div>
</div>
<div class="col-xs-12">
......@@ -82,7 +85,8 @@ export default {
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.
You can select a file in the left sidebar to begin
editing and use the right sidebar to commit your changes.
</p>
</div>
</div>
......
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import repoCommitSection from './repo_commit_section.vue';
import icon from '../../vue_shared/components/icon.vue';
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
import { mapGetters, mapState, mapActions } from 'vuex';
import repoCommitSection from './repo_commit_section.vue';
import icon from '../../vue_shared/components/icon.vue';
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
export default {
data() {
return {
width: 290,
};
},
export default {
components: {
repoCommitSection,
icon,
panelResizer,
},
data() {
return {
width: 290,
};
},
computed: {
...mapState([
'rightPanelCollapsed',
......@@ -53,7 +53,7 @@ export default {
this.setResizingStatus(false);
},
},
};
};
</script>
<template>
......@@ -64,8 +64,7 @@ export default {
}"
:style="panelStyle"
>
<div
class="multi-file-commit-panel-section">
<div class="multi-file-commit-panel-section">
<header
class="multi-file-commit-panel-header"
:class="{
......@@ -74,7 +73,8 @@ export default {
>
<div
class="multi-file-commit-panel-header-title"
v-if="!rightPanelCollapsed">
v-if="!rightPanelCollapsed"
>
<icon
name="list-bulleted"
:size="18"
......@@ -92,8 +92,7 @@ export default {
/>
</button>
</header>
<repo-commit-section
class=""/>
<repo-commit-section />
</div>
<panel-resizer
:size.sync="width"
......@@ -103,6 +102,7 @@ export default {
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="left"/>
side="left"
/>
</div>
</template>
......@@ -28,20 +28,20 @@ export default {
<div class="branch-header-title">
<icon
name="branch"
:size="12">
</icon>
:size="12"
/>
{{ branch.name }}
</div>
<div class="branch-header-btns">
<new-dropdown
:project-id="projectId"
:branch="branch.name"
path=""/>
path=""
/>
</div>
</div>
<div>
<repo-tree
:treeId="branch.treeId"/>
<repo-tree :tree-id="branch.treeId"/>
</div>
</div>
</template>
......@@ -21,7 +21,8 @@ export default {
<div class="context-header">
<a
:title="project.name"
:href="project.web_url">
:href="project.web_url"
>
<div class="avatar-container s40 project-avatar">
<project-avatar-image
class="avatar-container project-avatar"
......@@ -38,10 +39,11 @@ export default {
</div>
<div class="multi-file-commit-panel-inner-scroll">
<branches-tree
v-for="(branch, index) in project.branches"
v-for="branch in project.branches"
:key="branch.name"
:project-id="project.path_with_namespace"
:branch="branch"/>
:branch="branch"
/>
</div>
</div>
</template>
......@@ -44,21 +44,24 @@ export default {
</script>
<template>
<div>
<div>
<div class="ide-file-list">
<table class="table">
<tbody
v-if="treeId">
v-if="treeId"
>
<repo-previous-directory
v-if="hasPreviousDirectory"
/>
<template v-if="showLoading">
<div
class="multi-file-loading-container"
v-if="showLoading"
v-for="n in 3"
:key="n">
<skeleton-loading-container/>
:key="n"
>
<skeleton-loading-container />
</div>
</template>
<repo-file
v-for="file in fetchedList"
:key="file.key"
......@@ -67,5 +70,5 @@ export default {
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import projectTree from './ide_project_tree.vue';
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 { mapState, mapActions } from 'vuex';
import projectTree from './ide_project_tree.vue';
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';
export default {
data() {
return {
width: 290,
};
},
export default {
components: {
projectTree,
icon,
panelResizer,
skeletonLoadingContainer,
},
data() {
return {
width: 290,
};
},
computed: {
...mapState([
'loading',
......@@ -57,7 +57,7 @@ export default {
this.setResizingStatus(false);
},
},
};
};
</script>
<template>
......@@ -69,17 +69,19 @@ export default {
:style="panelStyle"
>
<div class="multi-file-commit-panel-inner">
<template v-if="showLoading">
<div
class="multi-file-loading-container"
v-if="showLoading"
v-for="n in 3"
:key="n">
<skeleton-loading-container/>
<skeleton-loading-container />
</div>
</template>
<project-tree
v-for="(project, index) in projects"
v-for="project in projects"
:key="project.id"
:project="project"/>
:project="project"
/>
</div>
<button
type="button"
......@@ -93,7 +95,9 @@ export default {
<span
v-if="!leftPanelCollapsed"
class="collapse-text"
>Collapse sidebar</span>
>
Collapse sidebar
</span>
</button>
<panel-resizer
:size.sync="width"
......@@ -103,6 +107,7 @@ export default {
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="right"/>
side="right"
/>
</div>
</template>
<script>
import { mapState } 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 { mapState } from 'vuex';
import icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
export default {
props: {
file: {
type: Object,
required: true,
},
},
export default {
components: {
icon,
},
......@@ -20,51 +14,52 @@ export default {
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
},
computed: {
...mapState([
'selectedFile',
]),
},
};
};
</script>
<template>
<div
class="ide-status-bar">
<div class="ide-status-bar">
<div>
<icon
name="branch"
:size="12">
</icon>
:size="12"
/>
{{ selectedFile.branchId }}
</div>
<div>
<div
v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
<div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
Last commit:
<a
v-tooltip
:title="selectedFile.lastCommit.message"
:href="selectedFile.lastCommit.url">
:href="selectedFile.lastCommit.url"
>
{{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
{{ selectedFile.lastCommit.author }}
</a>
</div>
</div>
<div
class="text-right">
<div class="text-right">
{{ selectedFile.name }}
</div>
<div
class="text-right">
<div class="text-right">
{{ selectedFile.eol }}
</div>
<div
class="text-right">
<div class="text-right">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div
class="text-right">
<div class="text-right">
{{ selectedFile.fileLanguage }}
</div>
</div>
......
......@@ -21,6 +21,13 @@
return this.loading || this.branchName === '';
},
},
created() {
// Dropdown is outside of Vue instance & is controlled by Bootstrap
this.$dropdown = $('.git-revision-dropdown');
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
},
methods: {
...mapActions([
'createNewBranch',
......@@ -55,13 +62,6 @@
}));
},
},
created() {
// Dropdown is outside of Vue instance & is controlled by Bootstrap
this.$dropdown = $('.git-revision-dropdown');
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
},
};
</script>
......
......@@ -4,6 +4,11 @@
import icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
icon,
newModal,
upload,
},
props: {
branch: {
type: String,
......@@ -18,11 +23,6 @@
default: null,
},
},
components: {
icon,
newModal,
upload,
},
data() {
return {
openModal: false,
......
......@@ -4,6 +4,9 @@
import modal from '../../../vue_shared/components/modal.vue';
export default {
components: {
modal,
},
props: {
branchId: {
type: String,
......@@ -27,28 +30,6 @@
entryName: this.path !== '' ? `${this.path}/` : '',
};
},
components: {
modal,
},
methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() {
this.createTempEntry({
projectId: this.currentProjectId,
branchId: this.branchId,
parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type,
});
this.hideModal();
},
hideModal() {
this.$emit('hide');
},
},
computed: {
...mapState([
'currentProjectId',
......@@ -78,6 +59,25 @@
mounted() {
this.$refs.fieldName.focus();
},
methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() {
this.createTempEntry({
projectId: this.currentProjectId,
branchId: this.branchId,
parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type,
});
this.hideModal();
},
hideModal() {
this.$emit('hide');
},
},
};
</script>
......
......@@ -18,6 +18,12 @@
'currentProjectId',
]),
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
beforeDestroy() {
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
methods: {
...mapActions([
'createTempEntry',
......@@ -59,12 +65,6 @@
this.$refs.fileUpload.click();
},
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
beforeDestroy() {
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
};
</script>
......
......@@ -49,7 +49,9 @@ export default {
const createNewBranch = newBranch || this.startNewMR;
const payload = {
branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId,
branch: createNewBranch ?
`${this.currentBranchId}-${new Date().getTime().toString()}` :
this.currentBranchId,
commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
......@@ -103,13 +105,14 @@ export default {
</script>
<template>
<div class="multi-file-commit-panel-section">
<div class="multi-file-commit-panel-section">
<modal
v-if="showNewBranchModal"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
:text="__(`This branch has changed since
you started editing. Would you like to create a new branch?`)"
@cancel="showNewBranchModal = false"
@submit="makeCommit(true)"
/>
......@@ -167,5 +170,5 @@ export default {
</div>
</div>
</form>
</div>
</div>
</template>
......@@ -40,7 +40,7 @@ export default {
aria-hidden="true">
</i>
<span>
{{buttonLabel}}
{{ buttonLabel }}
</span>
</button>
<modal
......
......@@ -6,6 +6,38 @@ import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
export default {
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'panelResizing',
]),
shouldHideEditor() {
return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
},
},
watch: {
activeFile(oldVal, newVal) {
if (newVal && !newVal.active) {
this.initMonaco();
}
},
leftPanelCollapsed() {
this.editor.updateDimensions();
},
rightPanelCollapsed() {
this.editor.updateDimensions();
},
panelResizing(isResizing) {
if (isResizing === false) {
this.editor.updateDimensions();
}
},
},
beforeDestroy() {
this.editor.dispose();
},
......@@ -78,38 +110,6 @@ export default {
});
},
},
watch: {
activeFile(oldVal, newVal) {
if (newVal && !newVal.active) {
this.initMonaco();
}
},
leftPanelCollapsed() {
this.editor.updateDimensions();
},
rightPanelCollapsed() {
this.editor.updateDimensions();
},
panelResizing(isResizing) {
if (isResizing === false) {
this.editor.updateDimensions();
}
},
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'panelResizing',
]),
shouldHideEditor() {
return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
},
},
};
</script>
......
......@@ -7,15 +7,15 @@
import fileIcon from '../../vue_shared/components/file_icon.vue';
export default {
mixins: [
timeAgoMixin,
],
components: {
skeletonLoadingContainer,
newDropdown,
fileStatusIcon,
fileIcon,
},
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
......@@ -62,6 +62,11 @@
};
},
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
}
},
methods: {
clickFile(row) {
// Manual Action if a tree is selected/opened
......@@ -74,11 +79,6 @@
this.$router.push(`/project${row.url}`);
},
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
}
},
};
</script>
......@@ -101,19 +101,17 @@
:opened="file.opened"
:style="levelIndentation"
:size="16"
>
</file-icon>
/>
{{ file.name }}
<file-status-icon
:file="file">
</file-status-icon>
<file-status-icon :file="file" />
</a>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
:parent="file"/>
:parent="file"
/>
<i
class="fa"
v-if="changedClass"
......
......@@ -35,20 +35,24 @@ export default {
<div
class="btn-group"
role="group"
aria-label="File actions">
aria-label="File actions"
>
<a
:href="activeFile.blamePath"
class="btn btn-default btn-sm blame">
class="btn btn-default btn-sm blame"
>
Blame
</a>
<a
:href="activeFile.commitsPath"
class="btn btn-default btn-sm history">
class="btn btn-default btn-sm history"
>
History
</a>
<a
:href="activeFile.permalink"
class="btn btn-default btn-sm permalink">
class="btn btn-default btn-sm permalink"
>
Permalink
</a>
</div>
......
......@@ -34,7 +34,6 @@
<icon
name="lock"
css-classes="file-status-icon"
>
</icon>
/>
</span>
</template>
......@@ -25,15 +25,13 @@
/>
</td>
<template v-if="!leftPanelCollapsed">
<td
class="hidden-sm hidden-xs">
<td class="hidden-sm hidden-xs">
<skeleton-loading-container
:small="true"
/>
</td>
<td
class="hidden-xs">
<td class="hidden-xs">
<skeleton-loading-container
class="animation-container-right"
:small="true"
......
<script>
import { mapGetters } from 'vuex';
import LineHighlighter from '../../line_highlighter';
import syntaxHighlight from '../../syntax_highlight';
import { mapGetters } from 'vuex';
import LineHighlighter from '../../line_highlighter';
import syntaxHighlight from '../../syntax_highlight';
export default {
export default {
computed: {
...mapGetters([
'activeFile',
......@@ -12,11 +12,6 @@ export default {
return this.activeFile.renderError === 'too_large';
},
},
methods: {
highlightFile() {
syntaxHighlight($(this.$el).find('.file-content'));
},
},
mounted() {
this.highlightFile();
this.lineHighlighter = new LineHighlighter({
......@@ -29,11 +24,16 @@ export default {
this.highlightFile();
});
},
};
methods: {
highlightFile() {
syntaxHighlight($(this.$el).find('.file-content'));
},
},
};
</script>
<template>
<div>
<div>
<div
v-if="!activeFile.renderError"
v-html="activeFile.html"
......@@ -51,15 +51,22 @@ export default {
v-else-if="renderErrorTooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead.
The source could not be displayed because it is too large.
You can <a
:href="activeFile.rawPath"
download>download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead.
The source could not be displayed because a rendering error occurred. You can
<a
:href="activeFile.rawPath"
download
>download</a> it instead.
</p>
</div>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import fileStatusIcon from './repo_file_status_icon.vue';
import fileIcon from '../../vue_shared/components/file_icon.vue';
import { mapActions } from 'vuex';
import fileStatusIcon from './repo_file_status_icon.vue';
import fileIcon from '../../vue_shared/components/file_icon.vue';
export default {
export default {
components: {
fileStatusIcon,
fileIcon,
},
props: {
tab: {
type: Object,
required: true,
},
},
components: {
fileStatusIcon,
fileIcon,
},
computed: {
closeLabel() {
if (this.tab.changed || this.tab.tempFile) {
......@@ -38,13 +38,11 @@ export default {
this.$router.push(`/project${tab.url}`);
},
},
};
};
</script>
<template>
<li
@click="clickFile(tab)"
>
<li @click="clickFile(tab)">
<button
type="button"
class="multi-file-tab-close"
......@@ -71,8 +69,7 @@ export default {
<file-icon
:file-name="tab.name"
:size="16"
>
</file-icon>
/>
{{ tab.name }}
<file-status-icon
:file="tab"
......
......@@ -6,7 +6,10 @@ import issueToken from './issue_token.vue';
export default {
name: 'AddIssuableForm',
components: {
issueToken,
loadingIcon,
},
props: {
inputValue: {
type: String,
......@@ -36,11 +39,6 @@ export default {
};
},
components: {
issueToken,
loadingIcon,
},
computed: {
inputPlaceholder() {
return `Paste issue link${this.allowAutoComplete ? ' or <#issue id>' : ''}`;
......@@ -54,6 +52,28 @@ export default {
},
},
mounted() {
const $input = $(this.$refs.input);
if (this.allowAutoComplete) {
this.gfmAutoComplete = new GfmAutoComplete(this.autoCompleteSources);
this.gfmAutoComplete.setup($input, {
issues: true,
});
$input.on('shown-issues.atwho', this.onAutoCompleteToggled.bind(this, true));
$input.on('hidden-issues.atwho', this.onAutoCompleteToggled.bind(this, false));
}
this.$refs.input.focus();
},
beforeDestroy() {
const $input = $(this.$refs.input);
$input.off('shown-issues.atwho');
$input.off('hidden-issues.atwho');
$input.off('inserted-issues.atwho', this.onInput);
},
methods: {
onInput() {
const value = this.$refs.input.value;
......@@ -85,28 +105,6 @@ export default {
eventHub.$emit('addIssuableFormCancel');
},
},
mounted() {
const $input = $(this.$refs.input);
if (this.allowAutoComplete) {
this.gfmAutoComplete = new GfmAutoComplete(this.autoCompleteSources);
this.gfmAutoComplete.setup($input, {
issues: true,
});
$input.on('shown-issues.atwho', this.onAutoCompleteToggled.bind(this, true));
$input.on('hidden-issues.atwho', this.onAutoCompleteToggled.bind(this, false));
}
this.$refs.input.focus();
},
beforeDestroy() {
const $input = $(this.$refs.input);
$input.off('shown-issues.atwho');
$input.off('hidden-issues.atwho');
$input.off('inserted-issues.atwho', this.onInput);
},
};
</script>
......
......@@ -32,13 +32,15 @@ export default {
</script>
<template>
<div :class="{
<div
:class="{
'issue-token': isCondensed,
'flex-row issue-info-container': !isCondensed,
}">
}"
>
<component
v-tooltip
:is="this.computedLinkElementType"
:is="computedLinkElementType"
ref="link"
:class="{
'issue-token-link': isCondensed,
......@@ -57,7 +59,8 @@ export default {
'issue-token-title issue-token-end': isCondensed,
'issue-title block-truncated': !isCondensed,
'issue-token-title-standalone': !canRemove
}">
}"
>
<span class="issue-token-title-text">
{{ title }}
</span>
......@@ -68,7 +71,8 @@ export default {
:class="{
'issue-token-reference': isCondensed,
'issuable-info': !isCondensed,
}">
}"
>
<icon
v-if="hasState"
v-tooltip
......@@ -78,7 +82,7 @@ export default {
:title="stateTitle"
:aria-label="state"
/>
</i>{{ displayReference }}
{{ displayReference }}
</component>
</component>
<button
......
......@@ -9,6 +9,14 @@ import addIssuableForm from './add_issuable_form.vue';
export default {
name: 'RelatedIssuesBlock',
directives: {
tooltip,
},
components: {
loadingIcon,
addIssuableForm,
issueItem,
},
props: {
isFetching: {
type: Boolean,
......@@ -66,14 +74,6 @@ export default {
default: 'Related issues',
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
addIssuableForm,
issueItem,
},
computed: {
hasRelatedIssues() {
return this.relatedIssues.length > 0;
......@@ -91,6 +91,14 @@ export default {
return this.helpPath.length > 0;
},
},
mounted() {
if (this.canReorder) {
this.sortable = Sortable.create(this.$refs.list, Object.assign({}, sortableConfig, {
onStart: this.addDraggingCursor,
onEnd: this.reordered,
}));
}
},
methods: {
toggleAddRelatedIssuesForm() {
eventHub.$emit('toggleAddRelatedIssuesForm');
......@@ -111,48 +119,50 @@ export default {
document.body.classList.remove('is-dragging');
},
},
mounted() {
if (this.canReorder) {
this.sortable = Sortable.create(this.$refs.list, Object.assign({}, sortableConfig, {
onStart: this.addDraggingCursor,
onEnd: this.reordered,
}));
}
},
};
</script>
<template>
<div class="related-issues-block">
<div
class="panel-slim panel-default">
class="panel-slim panel-default"
>
<div
class="panel-heading"
:class="{ 'panel-empty-heading': !this.hasBody }">
:class="{ 'panel-empty-heading': !hasBody }"
>
<h3 class="panel-title">
{{ title }}
<a
v-if="hasHelpPath"
:href="helpPath">
:href="helpPath"
>
<i
class="related-issues-header-help-icon fa fa-question-circle"
class="related-issues-header-help-icon
fa fa-question-circle"
aria-label="Read more about related issues">
</i>
</a>
<div class="js-related-issues-header-issue-count related-issues-header-issue-count issue-count-badge">
<div
class="js-related-issues-header-issue-count
related-issues-header-issue-count issue-count-badge"
>
<span
class="issue-count-badge-count"
:class="{ 'has-btn': this.canAdmin }">
:class="{ 'has-btn': canAdmin }"
>
{{ badgeLabel }}
</span>
<button
v-if="canAdmin"
ref="issueCountBadgeAddButton"
type="button"
class="js-issue-count-badge-add-button issue-count-badge-add-button btn btn-sm btn-default"
class="js-issue-count-badge-add-button
issue-count-badge-add-button btn btn-sm btn-default"
aria-label="Add an issue"
data-placement="top"
@click="toggleAddRelatedIssuesForm">
@click="toggleAddRelatedIssuesForm"
>
<i
class="fa fa-plus"
aria-hidden="true">
......@@ -166,25 +176,29 @@ export default {
class="js-add-related-issues-form-area panel-body"
:class="{
'related-issues-add-related-issues-form-with-break': hasRelatedIssues
}">
}"
>
<add-issuable-form
:is-submitting="isSubmitting"
:input-value="inputValue"
:pending-references="pendingReferences"
:auto-complete-sources="autoCompleteSources" />
:auto-complete-sources="autoCompleteSources"
/>
</div>
<div
class="related-issues-token-body panel-body"
:class="{
'collapsed': !shouldShowTokenBody,
'sortable-container': canReorder
}">
}"
>
<div
v-if="isFetching"
class="related-issues-loading-icon">
<loadingIcon
ref="loadingIcon"
label="Fetching related issues" />
label="Fetching related issues"
/>
</div>
<ul
ref="list"
......@@ -217,5 +231,4 @@ export default {
</div>
</div>
</div>
</div>
</template>
......@@ -34,6 +34,9 @@ const SPACE_FACTOR = 1;
export default {
name: 'RelatedIssuesRoot',
components: {
relatedIssuesBlock: RelatedIssuesBlock,
},
props: {
endpoint: {
type: String,
......@@ -76,15 +79,33 @@ export default {
inputValue: '',
};
},
components: {
relatedIssuesBlock: RelatedIssuesBlock,
},
computed: {
autoCompleteSources() {
if (!this.allowAutoComplete) return {};
return gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources;
},
},
created() {
eventHub.$on('relatedIssue-removeRequest', this.onRelatedIssueRemoveRequest);
eventHub.$on('toggleAddRelatedIssuesForm', this.onToggleAddRelatedIssuesForm);
eventHub.$on('pendingIssuable-removeRequest', this.onPendingIssueRemoveRequest);
eventHub.$on('addIssuableFormSubmit', this.onPendingFormSubmit);
eventHub.$on('addIssuableFormCancel', this.onPendingFormCancel);
eventHub.$on('addIssuableFormInput', this.onInput);
eventHub.$on('addIssuableFormBlur', this.onBlur);
this.service = new RelatedIssuesService(this.endpoint);
this.fetchRelatedIssues();
},
beforeDestroy() {
eventHub.$off('relatedIssue-removeRequest', this.onRelatedIssueRemoveRequest);
eventHub.$off('toggleAddRelatedIssuesForm', this.onToggleAddRelatedIssuesForm);
eventHub.$off('pendingIssuable-removeRequest', this.onPendingIssueRemoveRequest);
eventHub.$off('addIssuableFormSubmit', this.onPendingFormSubmit);
eventHub.$off('addIssuableFormCancel', this.onPendingFormCancel);
eventHub.$off('addIssuableFormInput', this.onInput);
eventHub.$off('addIssuableFormBlur', this.onBlur);
},
methods: {
onRelatedIssueRemoveRequest(idToRemove) {
const issueToRemove = _.find(this.state.relatedIssues, issue => issue.id === idToRemove);
......@@ -207,27 +228,6 @@ export default {
this.inputValue = '';
},
},
created() {
eventHub.$on('relatedIssue-removeRequest', this.onRelatedIssueRemoveRequest);
eventHub.$on('toggleAddRelatedIssuesForm', this.onToggleAddRelatedIssuesForm);
eventHub.$on('pendingIssuable-removeRequest', this.onPendingIssueRemoveRequest);
eventHub.$on('addIssuableFormSubmit', this.onPendingFormSubmit);
eventHub.$on('addIssuableFormCancel', this.onPendingFormCancel);
eventHub.$on('addIssuableFormInput', this.onInput);
eventHub.$on('addIssuableFormBlur', this.onBlur);
this.service = new RelatedIssuesService(this.endpoint);
this.fetchRelatedIssues();
},
beforeDestroy() {
eventHub.$off('relatedIssue-removeRequest', this.onRelatedIssueRemoveRequest);
eventHub.$off('toggleAddRelatedIssuesForm', this.onToggleAddRelatedIssuesForm);
eventHub.$off('pendingIssuable-removeRequest', this.onPendingIssueRemoveRequest);
eventHub.$off('addIssuableFormSubmit', this.onPendingFormSubmit);
eventHub.$off('addIssuableFormCancel', this.onPendingFormCancel);
eventHub.$off('addIssuableFormInput', this.onInput);
eventHub.$off('addIssuableFormBlur', this.onBlur);
},
};
</script>
......
<script>
import Visibility from 'visibilityjs';
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
import Visibility from 'visibilityjs';
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
export default {
components: {
descriptionComponent,
titleComponent,
editedComponent,
formComponent,
},
mixins: [
recaptchaModalImplementor,
],
props: {
endpoint: {
required: true,
......@@ -144,17 +153,40 @@ export default {
return !!this.state.updatedAt;
},
},
components: {
descriptionComponent,
titleComponent,
editedComponent,
formComponent,
created() {
this.service = new Service(this.endpoint);
this.poll = new Poll({
resource: this.service,
method: 'getData',
successCallback: res => this.store.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
});
mixins: [
recaptchaModalImplementor,
],
if (!Visibility.hidden()) {
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
},
beforeDestroy() {
eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
},
methods: {
openForm() {
if (!this.showForm) {
......@@ -220,45 +252,11 @@ export default {
});
},
},
created() {
this.service = new Service(this.endpoint);
this.poll = new Poll({
resource: this.service,
method: 'getData',
successCallback: res => this.store.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
},
beforeDestroy() {
eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
},
};
};
</script>
<template>
<div>
<div>
<div v-if="canUpdate && showForm">
<form-component
:form-state="formState"
......@@ -303,5 +301,5 @@ export default {
:updated-by-path="state.updatedByPath"
/>
</div>
</div>
</div>
</template>
......@@ -56,7 +56,10 @@
this.updateTaskStatusText();
},
},
mounted() {
this.renderGFM();
this.updateTaskStatusText();
},
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
......@@ -88,17 +91,17 @@
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
$tasksShort.text(
`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ?
's' :
''}`,
);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
},
mounted() {
this.renderGFM();
this.updateTaskStatusText();
},
};
</script>
......@@ -108,7 +111,8 @@
class="description"
:class="{
'js-task-list-container': canUpdate
}">
}"
>
<div
class="wiki"
:class="{
......
<script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default {
export default {
components: {
timeAgoTooltip,
},
props: {
updatedAt: {
type: String,
......@@ -19,15 +22,12 @@ export default {
default: '',
},
},
components: {
timeAgoTooltip,
},
computed: {
hasUpdatedBy() {
return this.updatedByName && this.updatedByPath;
},
},
};
};
</script>
<template>
......@@ -48,7 +48,7 @@ export default {
class="author_link"
:href="updatedByPath"
>
<span>{{updatedByName}}</span>
<span>{{ updatedByName }}</span>
</a>
</span>
</small>
......
......@@ -3,6 +3,9 @@
import markdownField from '../../../vue_shared/components/markdown/field.vue';
export default {
components: {
markdownField,
},
mixins: [updateMixin],
props: {
formState: {
......@@ -23,9 +26,6 @@
default: true,
},
},
components: {
markdownField,
},
mounted() {
this.$refs.textarea.focus();
},
......
......@@ -6,6 +6,13 @@
import descriptionTemplate from './fields/description_template.vue';
export default {
components: {
lockedWarning,
titleField,
descriptionField,
descriptionTemplate,
editActions,
},
props: {
canDestroy: {
type: Boolean,
......@@ -47,13 +54,6 @@
default: true,
},
},
components: {
lockedWarning,
titleField,
descriptionField,
descriptionTemplate,
editActions,
},
computed: {
hasIssuableTemplates() {
return this.issuableTemplates.length;
......@@ -73,16 +73,19 @@
:form-state="formState"
:issuable-templates="issuableTemplates"
:project-path="projectPath"
:project-namespace="projectNamespace" />
:project-namespace="projectNamespace"
/>
</div>
<div
:class="{
'col-sm-8 col-lg-9': hasIssuableTemplates,
'col-xs-12': !hasIssuableTemplates,
}">
}"
>
<title-field
:form-state="formState"
:issuable-templates="issuableTemplates" />
:issuable-templates="issuableTemplates"
/>
</div>
</div>
<description-field
......@@ -94,6 +97,7 @@
<edit-actions
:form-state="formState"
:can-destroy="canDestroy"
:show-delete-button="showDeleteButton" />
:show-delete-button="showDeleteButton"
/>
</form>
</template>
......@@ -5,14 +5,10 @@
import { spriteIcon } from '../../lib/utils/common_utils';
export default {
mixins: [animateMixin],
data() {
return {
preAnimation: false,
pulseAnimation: false,
titleEl: document.querySelector('title'),
};
directives: {
tooltip,
},
mixins: [animateMixin],
props: {
issuableRef: {
type: String,
......@@ -37,8 +33,17 @@
default: false,
},
},
directives: {
tooltip,
data() {
return {
preAnimation: false,
pulseAnimation: false,
titleEl: document.querySelector('title'),
};
},
computed: {
pencilIcon() {
return spriteIcon('pencil', 'link-highlight');
},
},
watch: {
titleHtml() {
......@@ -46,11 +51,6 @@
this.animateChange();
},
},
computed: {
pencilIcon() {
return spriteIcon('pencil', 'link-highlight');
},
},
methods: {
setPageTitle() {
const currentPageTitleScope = this.titleEl.innerText.split('·');
......
......@@ -3,7 +3,11 @@
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
name: 'jobHeaderSection',
name: 'JobHeaderSection',
components: {
ciHeader,
loadingIcon,
},
props: {
job: {
type: Object,
......@@ -14,10 +18,6 @@
required: true,
},
},
components: {
ciHeader,
loadingIcon,
},
data() {
return {
actions: this.getActions(),
......@@ -34,6 +34,11 @@
return this.job.started;
},
},
watch: {
job() {
this.actions = this.getActions();
},
},
methods: {
getActions() {
const actions = [];
......@@ -49,11 +54,6 @@
return actions;
},
},
watch: {
job() {
this.actions = this.getActions();
},
},
};
</script>
<template>
......
......@@ -23,9 +23,10 @@
<p class="build-detail-row">
<span
v-if="hasTitle"
class="build-light-text">
{{title}}:
class="build-light-text"
>
{{ title }}:
</span>
{{value}}
{{ value }}
</p>
</template>
......@@ -46,7 +46,6 @@ import LazyLoader from './lazy_loader';
import './line_highlighter';
import initLogoAnimation from './logo';
import './milestone_select';
import './preview_markdown';
import './projects_dropdown';
import './render_gfm';
import initBreadcrumbs from './breadcrumb';
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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