Merge branch 'master' into 28433-internationalise-cycle-analytics-page
This commit is contained in:
commit
302e855f52
3
Gemfile
3
Gemfile
|
@ -73,6 +73,9 @@ gem 'grape', '~> 0.19.0'
|
|||
gem 'grape-entity', '~> 0.6.0'
|
||||
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
|
||||
|
||||
# Disable strong_params so that Mash does not respond to :permitted?
|
||||
gem 'hashie-forbidden_attributes'
|
||||
|
||||
# Pagination
|
||||
gem 'kaminari', '~> 0.17.0'
|
||||
|
||||
|
|
|
@ -336,7 +336,7 @@ GEM
|
|||
grape-entity (0.6.0)
|
||||
activesupport
|
||||
multi_json (>= 1.3.2)
|
||||
grpc (1.2.2)
|
||||
grpc (1.1.2)
|
||||
google-protobuf (~> 3.1)
|
||||
googleauth (~> 0.5.1)
|
||||
haml (4.0.7)
|
||||
|
@ -352,6 +352,8 @@ GEM
|
|||
tilt
|
||||
hashdiff (0.3.2)
|
||||
hashie (3.5.5)
|
||||
hashie-forbidden_attributes (0.1.1)
|
||||
hashie (>= 3.0)
|
||||
health_check (2.6.0)
|
||||
rails (>= 4.0)
|
||||
hipchat (1.5.2)
|
||||
|
@ -925,6 +927,7 @@ DEPENDENCIES
|
|||
grape-entity (~> 0.6.0)
|
||||
haml_lint (~> 0.21.0)
|
||||
hamlit (~> 2.6.1)
|
||||
hashie-forbidden_attributes
|
||||
health_check (~> 2.6.0)
|
||||
hipchat (~> 1.5.0)
|
||||
html-pipeline (~> 1.11.0)
|
||||
|
|
|
@ -7,100 +7,98 @@ import boardBlankState from './board_blank_state';
|
|||
require('./board_delete');
|
||||
require('./board_list');
|
||||
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.Board = Vue.extend({
|
||||
template: '#js-board-template',
|
||||
components: {
|
||||
boardList,
|
||||
'board-delete': gl.issueBoards.BoardDelete,
|
||||
boardBlankState,
|
||||
},
|
||||
props: {
|
||||
list: Object,
|
||||
disabled: Boolean,
|
||||
issueLinkBase: String,
|
||||
rootPath: String,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
detailIssue: Store.detail,
|
||||
filter: Store.filter,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
filter: {
|
||||
handler() {
|
||||
this.list.page = 1;
|
||||
this.list.getIssues(true);
|
||||
},
|
||||
deep: true,
|
||||
gl.issueBoards.Board = Vue.extend({
|
||||
template: '#js-board-template',
|
||||
components: {
|
||||
boardList,
|
||||
'board-delete': gl.issueBoards.BoardDelete,
|
||||
boardBlankState,
|
||||
},
|
||||
props: {
|
||||
list: Object,
|
||||
disabled: Boolean,
|
||||
issueLinkBase: String,
|
||||
rootPath: String,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
detailIssue: Store.detail,
|
||||
filter: Store.filter,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
filter: {
|
||||
handler() {
|
||||
this.list.page = 1;
|
||||
this.list.getIssues(true);
|
||||
},
|
||||
detailIssue: {
|
||||
handler () {
|
||||
if (!Object.keys(this.detailIssue.issue).length) return;
|
||||
deep: true,
|
||||
},
|
||||
detailIssue: {
|
||||
handler () {
|
||||
if (!Object.keys(this.detailIssue.issue).length) return;
|
||||
|
||||
const issue = this.list.findIssue(this.detailIssue.issue.id);
|
||||
const issue = this.list.findIssue(this.detailIssue.issue.id);
|
||||
|
||||
if (issue) {
|
||||
const offsetLeft = this.$el.offsetLeft;
|
||||
const boardsList = document.querySelectorAll('.boards-list')[0];
|
||||
const left = boardsList.scrollLeft - offsetLeft;
|
||||
let right = (offsetLeft + this.$el.offsetWidth);
|
||||
if (issue) {
|
||||
const offsetLeft = this.$el.offsetLeft;
|
||||
const boardsList = document.querySelectorAll('.boards-list')[0];
|
||||
const left = boardsList.scrollLeft - offsetLeft;
|
||||
let right = (offsetLeft + this.$el.offsetWidth);
|
||||
|
||||
if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
|
||||
// -290 here because width of boardsList is animating so therefore
|
||||
// getting the width here is incorrect
|
||||
// 290 is the width of the sidebar
|
||||
right -= (boardsList.offsetWidth - 290);
|
||||
} else {
|
||||
right -= boardsList.offsetWidth;
|
||||
}
|
||||
|
||||
if (right - boardsList.scrollLeft > 0) {
|
||||
$(boardsList).animate({
|
||||
scrollLeft: right
|
||||
}, this.sortableOptions.animation);
|
||||
} else if (left > 0) {
|
||||
$(boardsList).animate({
|
||||
scrollLeft: offsetLeft
|
||||
}, this.sortableOptions.animation);
|
||||
}
|
||||
if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
|
||||
// -290 here because width of boardsList is animating so therefore
|
||||
// getting the width here is incorrect
|
||||
// 290 is the width of the sidebar
|
||||
right -= (boardsList.offsetWidth - 290);
|
||||
} else {
|
||||
right -= boardsList.offsetWidth;
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showNewIssueForm() {
|
||||
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
|
||||
disabled: this.disabled,
|
||||
group: 'boards',
|
||||
draggable: '.is-draggable',
|
||||
handle: '.js-board-handle',
|
||||
onEnd: (e) => {
|
||||
gl.issueBoards.onEnd();
|
||||
|
||||
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
|
||||
const order = this.sortable.toArray();
|
||||
const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
|
||||
|
||||
this.$nextTick(() => {
|
||||
Store.moveList(list, order);
|
||||
});
|
||||
if (right - boardsList.scrollLeft > 0) {
|
||||
$(boardsList).animate({
|
||||
scrollLeft: right
|
||||
}, this.sortableOptions.animation);
|
||||
} else if (left > 0) {
|
||||
$(boardsList).animate({
|
||||
scrollLeft: offsetLeft
|
||||
}, this.sortableOptions.animation);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showNewIssueForm() {
|
||||
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
|
||||
disabled: this.disabled,
|
||||
group: 'boards',
|
||||
draggable: '.is-draggable',
|
||||
handle: '.js-board-handle',
|
||||
onEnd: (e) => {
|
||||
gl.issueBoards.onEnd();
|
||||
|
||||
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
|
||||
},
|
||||
});
|
||||
})();
|
||||
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
|
||||
const order = this.sortable.toArray();
|
||||
const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
|
||||
|
||||
this.$nextTick(() => {
|
||||
Store.moveList(list, order);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,22 +2,20 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.BoardDelete = Vue.extend({
|
||||
props: {
|
||||
list: Object
|
||||
},
|
||||
methods: {
|
||||
deleteBoard () {
|
||||
$(this.$el).tooltip('hide');
|
||||
gl.issueBoards.BoardDelete = Vue.extend({
|
||||
props: {
|
||||
list: Object
|
||||
},
|
||||
methods: {
|
||||
deleteBoard () {
|
||||
$(this.$el).tooltip('hide');
|
||||
|
||||
if (confirm('Are you sure you want to delete this list?')) {
|
||||
this.list.destroy();
|
||||
}
|
||||
if (confirm('Are you sure you want to delete this list?')) {
|
||||
this.list.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -8,66 +8,64 @@ import Vue from 'vue';
|
|||
|
||||
require('./sidebar/remove_issue');
|
||||
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.BoardSidebar = Vue.extend({
|
||||
props: {
|
||||
currentUser: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
detail: Store.detail,
|
||||
issue: {},
|
||||
list: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showSidebar () {
|
||||
return Object.keys(this.issue).length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
detail: {
|
||||
handler () {
|
||||
if (this.issue.id !== this.detail.issue.id) {
|
||||
$('.js-issue-board-sidebar', this.$el).each((i, el) => {
|
||||
$(el).data('glDropdown').clearMenu();
|
||||
});
|
||||
}
|
||||
|
||||
this.issue = this.detail.issue;
|
||||
this.list = this.detail.list;
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
issue () {
|
||||
if (this.showSidebar) {
|
||||
this.$nextTick(() => {
|
||||
$('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
|
||||
$('.right-sidebar').getNiceScroll().resize();
|
||||
gl.issueBoards.BoardSidebar = Vue.extend({
|
||||
props: {
|
||||
currentUser: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
detail: Store.detail,
|
||||
issue: {},
|
||||
list: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showSidebar () {
|
||||
return Object.keys(this.issue).length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
detail: {
|
||||
handler () {
|
||||
if (this.issue.id !== this.detail.issue.id) {
|
||||
$('.js-issue-board-sidebar', this.$el).each((i, el) => {
|
||||
$(el).data('glDropdown').clearMenu();
|
||||
});
|
||||
}
|
||||
|
||||
this.issue = this.detail.issue;
|
||||
this.list = this.detail.list;
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
issue () {
|
||||
if (this.showSidebar) {
|
||||
this.$nextTick(() => {
|
||||
$('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
|
||||
$('.right-sidebar').getNiceScroll().resize();
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeSidebar () {
|
||||
this.detail.issue = {};
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
new IssuableContext(this.currentUser);
|
||||
new MilestoneSelect();
|
||||
new gl.DueDateSelectors();
|
||||
new LabelsSelect();
|
||||
new Sidebar();
|
||||
gl.Subscription.bindAll('.subscription');
|
||||
},
|
||||
components: {
|
||||
removeBtn: gl.issueBoards.RemoveIssueBtn,
|
||||
},
|
||||
});
|
||||
})();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeSidebar () {
|
||||
this.detail.issue = {};
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
new IssuableContext(this.currentUser);
|
||||
new MilestoneSelect();
|
||||
new gl.DueDateSelectors();
|
||||
new LabelsSelect();
|
||||
new Sidebar();
|
||||
gl.Subscription.bindAll('.subscription');
|
||||
},
|
||||
components: {
|
||||
removeBtn: gl.issueBoards.RemoveIssueBtn,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,141 +1,139 @@
|
|||
import Vue from 'vue';
|
||||
import eventHub from '../eventhub';
|
||||
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.IssueCardInner = Vue.extend({
|
||||
props: {
|
||||
issue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
issueLinkBase: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
list: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
rootPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
updateFilters: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
gl.issueBoards.IssueCardInner = Vue.extend({
|
||||
props: {
|
||||
issue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
cardUrl() {
|
||||
return `${this.issueLinkBase}/${this.issue.id}`;
|
||||
},
|
||||
assigneeUrl() {
|
||||
return `${this.rootPath}${this.issue.assignee.username}`;
|
||||
},
|
||||
assigneeUrlTitle() {
|
||||
return `Assigned to ${this.issue.assignee.name}`;
|
||||
},
|
||||
avatarUrlTitle() {
|
||||
return `Avatar for ${this.issue.assignee.name}`;
|
||||
},
|
||||
issueId() {
|
||||
return `#${this.issue.id}`;
|
||||
},
|
||||
showLabelFooter() {
|
||||
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
|
||||
},
|
||||
issueLinkBase: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
methods: {
|
||||
showLabel(label) {
|
||||
if (!this.list) return true;
|
||||
|
||||
return !this.list.label || label.id !== this.list.label.id;
|
||||
},
|
||||
filterByLabel(label, e) {
|
||||
if (!this.updateFilters) return;
|
||||
|
||||
const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
|
||||
const labelTitle = encodeURIComponent(label.title);
|
||||
const param = `label_name[]=${labelTitle}`;
|
||||
const labelIndex = filterPath.indexOf(param);
|
||||
$(e.currentTarget).tooltip('hide');
|
||||
|
||||
if (labelIndex === -1) {
|
||||
filterPath.push(param);
|
||||
} else {
|
||||
filterPath.splice(labelIndex, 1);
|
||||
}
|
||||
|
||||
gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
|
||||
|
||||
Store.updateFiltersUrl();
|
||||
|
||||
eventHub.$emit('updateTokens');
|
||||
},
|
||||
labelStyle(label) {
|
||||
return {
|
||||
backgroundColor: label.color,
|
||||
color: label.textColor,
|
||||
};
|
||||
},
|
||||
list: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">
|
||||
<i
|
||||
class="fa fa-eye-slash confidential-icon"
|
||||
v-if="issue.confidential"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<a
|
||||
class="js-no-trigger"
|
||||
:href="cardUrl"
|
||||
:title="issue.title">{{ issue.title }}</a>
|
||||
<span
|
||||
class="card-number"
|
||||
v-if="issue.id"
|
||||
>
|
||||
{{ issueId }}
|
||||
</span>
|
||||
</h4>
|
||||
rootPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
updateFilters: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cardUrl() {
|
||||
return `${this.issueLinkBase}/${this.issue.id}`;
|
||||
},
|
||||
assigneeUrl() {
|
||||
return `${this.rootPath}${this.issue.assignee.username}`;
|
||||
},
|
||||
assigneeUrlTitle() {
|
||||
return `Assigned to ${this.issue.assignee.name}`;
|
||||
},
|
||||
avatarUrlTitle() {
|
||||
return `Avatar for ${this.issue.assignee.name}`;
|
||||
},
|
||||
issueId() {
|
||||
return `#${this.issue.id}`;
|
||||
},
|
||||
showLabelFooter() {
|
||||
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showLabel(label) {
|
||||
if (!this.list) return true;
|
||||
|
||||
return !this.list.label || label.id !== this.list.label.id;
|
||||
},
|
||||
filterByLabel(label, e) {
|
||||
if (!this.updateFilters) return;
|
||||
|
||||
const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
|
||||
const labelTitle = encodeURIComponent(label.title);
|
||||
const param = `label_name[]=${labelTitle}`;
|
||||
const labelIndex = filterPath.indexOf(param);
|
||||
$(e.currentTarget).tooltip('hide');
|
||||
|
||||
if (labelIndex === -1) {
|
||||
filterPath.push(param);
|
||||
} else {
|
||||
filterPath.splice(labelIndex, 1);
|
||||
}
|
||||
|
||||
gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
|
||||
|
||||
Store.updateFiltersUrl();
|
||||
|
||||
eventHub.$emit('updateTokens');
|
||||
},
|
||||
labelStyle(label) {
|
||||
return {
|
||||
backgroundColor: label.color,
|
||||
color: label.textColor,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">
|
||||
<i
|
||||
class="fa fa-eye-slash confidential-icon"
|
||||
v-if="issue.confidential"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<a
|
||||
class="card-assignee has-tooltip js-no-trigger"
|
||||
:href="assigneeUrl"
|
||||
:title="assigneeUrlTitle"
|
||||
v-if="issue.assignee"
|
||||
data-container="body"
|
||||
class="js-no-trigger"
|
||||
:href="cardUrl"
|
||||
:title="issue.title">{{ issue.title }}</a>
|
||||
<span
|
||||
class="card-number"
|
||||
v-if="issue.id"
|
||||
>
|
||||
<img
|
||||
class="avatar avatar-inline s20 js-no-trigger"
|
||||
:src="issue.assignee.avatar"
|
||||
width="20"
|
||||
height="20"
|
||||
:alt="avatarUrlTitle"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-footer" v-if="showLabelFooter">
|
||||
<button
|
||||
class="label color-label has-tooltip js-no-trigger"
|
||||
v-for="label in issue.labels"
|
||||
type="button"
|
||||
v-if="showLabel(label)"
|
||||
@click="filterByLabel(label, $event)"
|
||||
:style="labelStyle(label)"
|
||||
:title="label.description"
|
||||
data-container="body">
|
||||
{{ label.title }}
|
||||
</button>
|
||||
</div>
|
||||
{{ issueId }}
|
||||
</span>
|
||||
</h4>
|
||||
<a
|
||||
class="card-assignee has-tooltip js-no-trigger"
|
||||
:href="assigneeUrl"
|
||||
:title="assigneeUrlTitle"
|
||||
v-if="issue.assignee"
|
||||
data-container="body"
|
||||
>
|
||||
<img
|
||||
class="avatar avatar-inline s20 js-no-trigger"
|
||||
:src="issue.assignee.avatar"
|
||||
width="20"
|
||||
height="20"
|
||||
:alt="avatarUrlTitle"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
<div class="card-footer" v-if="showLabelFooter">
|
||||
<button
|
||||
class="label color-label has-tooltip js-no-trigger"
|
||||
v-for="label in issue.labels"
|
||||
type="button"
|
||||
v-if="showLabel(label)"
|
||||
@click="filterByLabel(label, $event)"
|
||||
:style="labelStyle(label)"
|
||||
:title="label.description"
|
||||
data-container="body">
|
||||
{{ label.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,71 +1,69 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
gl.issueBoards.ModalEmptyState = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
gl.issueBoards.ModalEmptyState = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
props: {
|
||||
image: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
image: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
newIssuePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
newIssuePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
contents() {
|
||||
const obj = {
|
||||
title: 'You haven\'t added any issues to your project yet',
|
||||
content: `
|
||||
An issue can be a bug, a todo or a feature request that needs to be
|
||||
discussed in a project. Besides, issues are searchable and filterable.
|
||||
`,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
contents() {
|
||||
const obj = {
|
||||
title: 'You haven\'t added any issues to your project yet',
|
||||
content: `
|
||||
An issue can be a bug, a todo or a feature request that needs to be
|
||||
discussed in a project. Besides, issues are searchable and filterable.
|
||||
`,
|
||||
};
|
||||
|
||||
if (this.activeTab === 'selected') {
|
||||
obj.title = 'You haven\'t selected any issues yet';
|
||||
obj.content = `
|
||||
Go back to <strong>Open issues</strong> and select some issues
|
||||
to add to your board.
|
||||
`;
|
||||
}
|
||||
if (this.activeTab === 'selected') {
|
||||
obj.title = 'You haven\'t selected any issues yet';
|
||||
obj.content = `
|
||||
Go back to <strong>Open issues</strong> and select some issues
|
||||
to add to your board.
|
||||
`;
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
return obj;
|
||||
},
|
||||
template: `
|
||||
<section class="empty-state">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-6 col-sm-push-6">
|
||||
<aside class="svg-content" v-html="image"></aside>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-6 col-sm-pull-6">
|
||||
<div class="text-content">
|
||||
<h4>{{ contents.title }}</h4>
|
||||
<p v-html="contents.content"></p>
|
||||
<a
|
||||
:href="newIssuePath"
|
||||
class="btn btn-success btn-inverted"
|
||||
v-if="activeTab === 'all'">
|
||||
New issue
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
@click="changeTab('all')"
|
||||
v-if="activeTab === 'selected'">
|
||||
Open issues
|
||||
</button>
|
||||
</div>
|
||||
},
|
||||
template: `
|
||||
<section class="empty-state">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-6 col-sm-push-6">
|
||||
<aside class="svg-content" v-html="image"></aside>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-6 col-sm-pull-6">
|
||||
<div class="text-content">
|
||||
<h4>{{ contents.title }}</h4>
|
||||
<p v-html="contents.content"></p>
|
||||
<a
|
||||
:href="newIssuePath"
|
||||
class="btn btn-success btn-inverted"
|
||||
v-if="activeTab === 'all'">
|
||||
New issue
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
@click="changeTab('all')"
|
||||
v-if="activeTab === 'selected'">
|
||||
Open issues
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -5,80 +5,78 @@ import Vue from 'vue';
|
|||
|
||||
require('./lists_dropdown');
|
||||
|
||||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
gl.issueBoards.ModalFooter = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
state: gl.issueBoards.BoardsStore.state,
|
||||
};
|
||||
gl.issueBoards.ModalFooter = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
state: gl.issueBoards.BoardsStore.state,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
submitDisabled() {
|
||||
return !ModalStore.selectedCount();
|
||||
},
|
||||
computed: {
|
||||
submitDisabled() {
|
||||
return !ModalStore.selectedCount();
|
||||
},
|
||||
submitText() {
|
||||
const count = ModalStore.selectedCount();
|
||||
submitText() {
|
||||
const count = ModalStore.selectedCount();
|
||||
|
||||
return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
|
||||
},
|
||||
return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
|
||||
},
|
||||
methods: {
|
||||
addIssues() {
|
||||
const list = this.modal.selectedList || this.state.lists[0];
|
||||
const selectedIssues = ModalStore.getSelectedIssues();
|
||||
const issueIds = selectedIssues.map(issue => issue.globalId);
|
||||
},
|
||||
methods: {
|
||||
addIssues() {
|
||||
const list = this.modal.selectedList || this.state.lists[0];
|
||||
const selectedIssues = ModalStore.getSelectedIssues();
|
||||
const issueIds = selectedIssues.map(issue => issue.globalId);
|
||||
|
||||
// Post the data to the backend
|
||||
gl.boardService.bulkUpdate(issueIds, {
|
||||
add_label_ids: [list.label.id],
|
||||
}).catch(() => {
|
||||
new Flash('Failed to update issues, please try again.', 'alert');
|
||||
// Post the data to the backend
|
||||
gl.boardService.bulkUpdate(issueIds, {
|
||||
add_label_ids: [list.label.id],
|
||||
}).catch(() => {
|
||||
new Flash('Failed to update issues, please try again.', 'alert');
|
||||
|
||||
selectedIssues.forEach((issue) => {
|
||||
list.removeIssue(issue);
|
||||
list.issuesSize -= 1;
|
||||
});
|
||||
});
|
||||
|
||||
// Add the issues on the frontend
|
||||
selectedIssues.forEach((issue) => {
|
||||
list.addIssue(issue);
|
||||
list.issuesSize += 1;
|
||||
list.removeIssue(issue);
|
||||
list.issuesSize -= 1;
|
||||
});
|
||||
});
|
||||
|
||||
this.toggleModal(false);
|
||||
},
|
||||
// Add the issues on the frontend
|
||||
selectedIssues.forEach((issue) => {
|
||||
list.addIssue(issue);
|
||||
list.issuesSize += 1;
|
||||
});
|
||||
|
||||
this.toggleModal(false);
|
||||
},
|
||||
components: {
|
||||
'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
|
||||
},
|
||||
template: `
|
||||
<footer
|
||||
class="form-actions add-issues-footer">
|
||||
<div class="pull-left">
|
||||
<button
|
||||
class="btn btn-success"
|
||||
type="button"
|
||||
:disabled="submitDisabled"
|
||||
@click="addIssues">
|
||||
{{ submitText }}
|
||||
</button>
|
||||
<span class="inline add-issues-footer-to-list">
|
||||
to list
|
||||
</span>
|
||||
<lists-dropdown></lists-dropdown>
|
||||
</div>
|
||||
},
|
||||
components: {
|
||||
'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
|
||||
},
|
||||
template: `
|
||||
<footer
|
||||
class="form-actions add-issues-footer">
|
||||
<div class="pull-left">
|
||||
<button
|
||||
class="btn btn-default pull-right"
|
||||
class="btn btn-success"
|
||||
type="button"
|
||||
@click="toggleModal(false)">
|
||||
Cancel
|
||||
:disabled="submitDisabled"
|
||||
@click="addIssues">
|
||||
{{ submitText }}
|
||||
</button>
|
||||
</footer>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
<span class="inline add-issues-footer-to-list">
|
||||
to list
|
||||
</span>
|
||||
<lists-dropdown></lists-dropdown>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-default pull-right"
|
||||
type="button"
|
||||
@click="toggleModal(false)">
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -3,80 +3,78 @@ import modalFilters from './filters';
|
|||
|
||||
require('./tabs');
|
||||
|
||||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
gl.issueBoards.ModalHeader = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
props: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
gl.issueBoards.ModalHeader = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
props: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
selectAllText() {
|
||||
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
|
||||
return 'Select all';
|
||||
}
|
||||
labelPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
computed: {
|
||||
selectAllText() {
|
||||
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
|
||||
return 'Select all';
|
||||
}
|
||||
|
||||
return 'Deselect all';
|
||||
},
|
||||
showSearch() {
|
||||
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
|
||||
},
|
||||
return 'Deselect all';
|
||||
},
|
||||
methods: {
|
||||
toggleAll() {
|
||||
this.$refs.selectAllBtn.blur();
|
||||
showSearch() {
|
||||
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleAll() {
|
||||
this.$refs.selectAllBtn.blur();
|
||||
|
||||
ModalStore.toggleAll();
|
||||
},
|
||||
ModalStore.toggleAll();
|
||||
},
|
||||
components: {
|
||||
'modal-tabs': gl.issueBoards.ModalTabs,
|
||||
modalFilters,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<header class="add-issues-header form-actions">
|
||||
<h2>
|
||||
Add issues
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
@click="toggleModal(false)">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</h2>
|
||||
</header>
|
||||
<modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
|
||||
<div
|
||||
class="add-issues-search append-bottom-10"
|
||||
v-if="showSearch">
|
||||
<modal-filters :store="filter" />
|
||||
},
|
||||
components: {
|
||||
'modal-tabs': gl.issueBoards.ModalTabs,
|
||||
modalFilters,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<header class="add-issues-header form-actions">
|
||||
<h2>
|
||||
Add issues
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-inverted prepend-left-10"
|
||||
ref="selectAllBtn"
|
||||
@click="toggleAll">
|
||||
{{ selectAllText }}
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
@click="toggleModal(false)">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</h2>
|
||||
</header>
|
||||
<modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
|
||||
<div
|
||||
class="add-issues-search append-bottom-10"
|
||||
v-if="showSearch">
|
||||
<modal-filters :store="filter" />
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-inverted prepend-left-10"
|
||||
ref="selectAllBtn"
|
||||
@click="toggleAll">
|
||||
{{ selectAllText }}
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -8,160 +8,158 @@ require('./list');
|
|||
require('./footer');
|
||||
require('./empty_state');
|
||||
|
||||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
gl.issueBoards.IssuesModal = Vue.extend({
|
||||
props: {
|
||||
blankStateImage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
newIssuePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
issueLinkBase: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rootPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
gl.issueBoards.IssuesModal = Vue.extend({
|
||||
props: {
|
||||
blankStateImage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
newIssuePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
watch: {
|
||||
page() {
|
||||
this.loadIssues();
|
||||
},
|
||||
showAddIssuesModal() {
|
||||
if (this.showAddIssuesModal && !this.issues.length) {
|
||||
this.loading = true;
|
||||
|
||||
this.loadIssues()
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
} else if (!this.showAddIssuesModal) {
|
||||
this.issues = [];
|
||||
this.selectedIssues = [];
|
||||
this.issuesCount = false;
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
handler() {
|
||||
if (this.$el.tagName) {
|
||||
this.page = 1;
|
||||
this.filterLoading = true;
|
||||
|
||||
this.loadIssues(true)
|
||||
.then(() => {
|
||||
this.filterLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
issueLinkBase: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
methods: {
|
||||
loadIssues(clearIssues = false) {
|
||||
if (!this.showAddIssuesModal) return false;
|
||||
rootPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
watch: {
|
||||
page() {
|
||||
this.loadIssues();
|
||||
},
|
||||
showAddIssuesModal() {
|
||||
if (this.showAddIssuesModal && !this.issues.length) {
|
||||
this.loading = true;
|
||||
|
||||
return gl.boardService.getBacklog(queryData(this.filter.path, {
|
||||
page: this.page,
|
||||
per: this.perPage,
|
||||
})).then((res) => {
|
||||
const data = res.json();
|
||||
|
||||
if (clearIssues) {
|
||||
this.issues = [];
|
||||
}
|
||||
|
||||
data.issues.forEach((issueObj) => {
|
||||
const issue = new ListIssue(issueObj);
|
||||
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
|
||||
issue.selected = !!foundSelectedIssue;
|
||||
|
||||
this.issues.push(issue);
|
||||
this.loadIssues()
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
} else if (!this.showAddIssuesModal) {
|
||||
this.issues = [];
|
||||
this.selectedIssues = [];
|
||||
this.issuesCount = false;
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
handler() {
|
||||
if (this.$el.tagName) {
|
||||
this.page = 1;
|
||||
this.filterLoading = true;
|
||||
|
||||
this.loadingNewPage = false;
|
||||
this.loadIssues(true)
|
||||
.then(() => {
|
||||
this.filterLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadIssues(clearIssues = false) {
|
||||
if (!this.showAddIssuesModal) return false;
|
||||
|
||||
if (!this.issuesCount) {
|
||||
this.issuesCount = data.size;
|
||||
}
|
||||
return gl.boardService.getBacklog(queryData(this.filter.path, {
|
||||
page: this.page,
|
||||
per: this.perPage,
|
||||
})).then((res) => {
|
||||
const data = res.json();
|
||||
|
||||
if (clearIssues) {
|
||||
this.issues = [];
|
||||
}
|
||||
|
||||
data.issues.forEach((issueObj) => {
|
||||
const issue = new ListIssue(issueObj);
|
||||
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
|
||||
issue.selected = !!foundSelectedIssue;
|
||||
|
||||
this.issues.push(issue);
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showList() {
|
||||
if (this.activeTab === 'selected') {
|
||||
return this.selectedIssues.length > 0;
|
||||
}
|
||||
|
||||
return this.issuesCount > 0;
|
||||
},
|
||||
showEmptyState() {
|
||||
if (!this.loading && this.issuesCount === 0) {
|
||||
return true;
|
||||
}
|
||||
this.loadingNewPage = false;
|
||||
|
||||
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
|
||||
},
|
||||
if (!this.issuesCount) {
|
||||
this.issuesCount = data.size;
|
||||
}
|
||||
});
|
||||
},
|
||||
created() {
|
||||
this.page = 1;
|
||||
},
|
||||
computed: {
|
||||
showList() {
|
||||
if (this.activeTab === 'selected') {
|
||||
return this.selectedIssues.length > 0;
|
||||
}
|
||||
|
||||
return this.issuesCount > 0;
|
||||
},
|
||||
components: {
|
||||
'modal-header': gl.issueBoards.ModalHeader,
|
||||
'modal-list': gl.issueBoards.ModalList,
|
||||
'modal-footer': gl.issueBoards.ModalFooter,
|
||||
'empty-state': gl.issueBoards.ModalEmptyState,
|
||||
showEmptyState() {
|
||||
if (!this.loading && this.issuesCount === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="add-issues-modal"
|
||||
v-if="showAddIssuesModal">
|
||||
<div class="add-issues-container">
|
||||
<modal-header
|
||||
:project-id="projectId"
|
||||
:milestone-path="milestonePath"
|
||||
:label-path="labelPath">
|
||||
</modal-header>
|
||||
<modal-list
|
||||
:image="blankStateImage"
|
||||
:issue-link-base="issueLinkBase"
|
||||
:root-path="rootPath"
|
||||
v-if="!loading && showList && !filterLoading"></modal-list>
|
||||
<empty-state
|
||||
v-if="showEmptyState"
|
||||
:image="blankStateImage"
|
||||
:new-issue-path="newIssuePath"></empty-state>
|
||||
<section
|
||||
class="add-issues-list text-center"
|
||||
v-if="loading || filterLoading">
|
||||
<div class="add-issues-list-loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</section>
|
||||
<modal-footer></modal-footer>
|
||||
</div>
|
||||
},
|
||||
created() {
|
||||
this.page = 1;
|
||||
},
|
||||
components: {
|
||||
'modal-header': gl.issueBoards.ModalHeader,
|
||||
'modal-list': gl.issueBoards.ModalList,
|
||||
'modal-footer': gl.issueBoards.ModalFooter,
|
||||
'empty-state': gl.issueBoards.ModalEmptyState,
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="add-issues-modal"
|
||||
v-if="showAddIssuesModal">
|
||||
<div class="add-issues-container">
|
||||
<modal-header
|
||||
:project-id="projectId"
|
||||
:milestone-path="milestonePath"
|
||||
:label-path="labelPath">
|
||||
</modal-header>
|
||||
<modal-list
|
||||
:image="blankStateImage"
|
||||
:issue-link-base="issueLinkBase"
|
||||
:root-path="rootPath"
|
||||
v-if="!loading && showList && !filterLoading"></modal-list>
|
||||
<empty-state
|
||||
v-if="showEmptyState"
|
||||
:image="blankStateImage"
|
||||
:new-issue-path="newIssuePath"></empty-state>
|
||||
<section
|
||||
class="add-issues-list text-center"
|
||||
v-if="loading || filterLoading">
|
||||
<div class="add-issues-list-loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</section>
|
||||
<modal-footer></modal-footer>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -3,159 +3,157 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
|
||||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
gl.issueBoards.ModalList = Vue.extend({
|
||||
props: {
|
||||
issueLinkBase: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rootPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
gl.issueBoards.ModalList = Vue.extend({
|
||||
props: {
|
||||
issueLinkBase: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
rootPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
watch: {
|
||||
activeTab() {
|
||||
if (this.activeTab === 'all') {
|
||||
ModalStore.purgeUnselectedIssues();
|
||||
}
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
loopIssues() {
|
||||
if (this.activeTab === 'all') {
|
||||
return this.issues;
|
||||
},
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
watch: {
|
||||
activeTab() {
|
||||
if (this.activeTab === 'all') {
|
||||
ModalStore.purgeUnselectedIssues();
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
loopIssues() {
|
||||
if (this.activeTab === 'all') {
|
||||
return this.issues;
|
||||
}
|
||||
|
||||
return this.selectedIssues;
|
||||
},
|
||||
groupedIssues() {
|
||||
const groups = [];
|
||||
this.loopIssues.forEach((issue, i) => {
|
||||
const index = i % this.columns;
|
||||
|
||||
if (!groups[index]) {
|
||||
groups.push([]);
|
||||
}
|
||||
|
||||
return this.selectedIssues;
|
||||
},
|
||||
groupedIssues() {
|
||||
const groups = [];
|
||||
this.loopIssues.forEach((issue, i) => {
|
||||
const index = i % this.columns;
|
||||
groups[index].push(issue);
|
||||
});
|
||||
|
||||
if (!groups[index]) {
|
||||
groups.push([]);
|
||||
}
|
||||
|
||||
groups[index].push(issue);
|
||||
});
|
||||
|
||||
return groups;
|
||||
},
|
||||
return groups;
|
||||
},
|
||||
methods: {
|
||||
scrollHandler() {
|
||||
const currentPage = Math.floor(this.issues.length / this.perPage);
|
||||
},
|
||||
methods: {
|
||||
scrollHandler() {
|
||||
const currentPage = Math.floor(this.issues.length / this.perPage);
|
||||
|
||||
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
|
||||
&& currentPage === this.page) {
|
||||
this.loadingNewPage = true;
|
||||
this.page += 1;
|
||||
}
|
||||
},
|
||||
toggleIssue(e, issue) {
|
||||
if (e.target.tagName !== 'A') {
|
||||
ModalStore.toggleIssue(issue);
|
||||
}
|
||||
},
|
||||
listHeight() {
|
||||
return this.$refs.list.getBoundingClientRect().height;
|
||||
},
|
||||
scrollHeight() {
|
||||
return this.$refs.list.scrollHeight;
|
||||
},
|
||||
scrollTop() {
|
||||
return this.$refs.list.scrollTop + this.listHeight();
|
||||
},
|
||||
showIssue(issue) {
|
||||
if (this.activeTab === 'all') return true;
|
||||
|
||||
const index = ModalStore.selectedIssueIndex(issue);
|
||||
|
||||
return index !== -1;
|
||||
},
|
||||
setColumnCount() {
|
||||
const breakpoint = bp.getBreakpointSize();
|
||||
|
||||
if (breakpoint === 'lg' || breakpoint === 'md') {
|
||||
this.columns = 3;
|
||||
} else if (breakpoint === 'sm') {
|
||||
this.columns = 2;
|
||||
} else {
|
||||
this.columns = 1;
|
||||
}
|
||||
},
|
||||
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
|
||||
&& currentPage === this.page) {
|
||||
this.loadingNewPage = true;
|
||||
this.page += 1;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.scrollHandlerWrapper = this.scrollHandler.bind(this);
|
||||
this.setColumnCountWrapper = this.setColumnCount.bind(this);
|
||||
this.setColumnCount();
|
||||
toggleIssue(e, issue) {
|
||||
if (e.target.tagName !== 'A') {
|
||||
ModalStore.toggleIssue(issue);
|
||||
}
|
||||
},
|
||||
listHeight() {
|
||||
return this.$refs.list.getBoundingClientRect().height;
|
||||
},
|
||||
scrollHeight() {
|
||||
return this.$refs.list.scrollHeight;
|
||||
},
|
||||
scrollTop() {
|
||||
return this.$refs.list.scrollTop + this.listHeight();
|
||||
},
|
||||
showIssue(issue) {
|
||||
if (this.activeTab === 'all') return true;
|
||||
|
||||
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
|
||||
window.addEventListener('resize', this.setColumnCountWrapper);
|
||||
const index = ModalStore.selectedIssueIndex(issue);
|
||||
|
||||
return index !== -1;
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
|
||||
window.removeEventListener('resize', this.setColumnCountWrapper);
|
||||
setColumnCount() {
|
||||
const breakpoint = bp.getBreakpointSize();
|
||||
|
||||
if (breakpoint === 'lg' || breakpoint === 'md') {
|
||||
this.columns = 3;
|
||||
} else if (breakpoint === 'sm') {
|
||||
this.columns = 2;
|
||||
} else {
|
||||
this.columns = 1;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'issue-card-inner': gl.issueBoards.IssueCardInner,
|
||||
},
|
||||
template: `
|
||||
<section
|
||||
class="add-issues-list add-issues-list-columns"
|
||||
ref="list">
|
||||
},
|
||||
mounted() {
|
||||
this.scrollHandlerWrapper = this.scrollHandler.bind(this);
|
||||
this.setColumnCountWrapper = this.setColumnCount.bind(this);
|
||||
this.setColumnCount();
|
||||
|
||||
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
|
||||
window.addEventListener('resize', this.setColumnCountWrapper);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
|
||||
window.removeEventListener('resize', this.setColumnCountWrapper);
|
||||
},
|
||||
components: {
|
||||
'issue-card-inner': gl.issueBoards.IssueCardInner,
|
||||
},
|
||||
template: `
|
||||
<section
|
||||
class="add-issues-list add-issues-list-columns"
|
||||
ref="list">
|
||||
<div
|
||||
class="empty-state add-issues-empty-state-filter text-center"
|
||||
v-if="issuesCount > 0 && issues.length === 0">
|
||||
<div
|
||||
class="empty-state add-issues-empty-state-filter text-center"
|
||||
v-if="issuesCount > 0 && issues.length === 0">
|
||||
class="svg-content"
|
||||
v-html="image">
|
||||
</div>
|
||||
<div class="text-content">
|
||||
<h4>
|
||||
There are no issues to show.
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="group in groupedIssues"
|
||||
class="add-issues-list-column">
|
||||
<div
|
||||
v-for="issue in group"
|
||||
v-if="showIssue(issue)"
|
||||
class="card-parent">
|
||||
<div
|
||||
class="svg-content"
|
||||
v-html="image">
|
||||
</div>
|
||||
<div class="text-content">
|
||||
<h4>
|
||||
There are no issues to show.
|
||||
</h4>
|
||||
class="card"
|
||||
:class="{ 'is-active': issue.selected }"
|
||||
@click="toggleIssue($event, issue)">
|
||||
<issue-card-inner
|
||||
:issue="issue"
|
||||
:issue-link-base="issueLinkBase"
|
||||
:root-path="rootPath">
|
||||
</issue-card-inner>
|
||||
<span
|
||||
:aria-label="'Issue #' + issue.id + ' selected'"
|
||||
aria-checked="true"
|
||||
v-if="issue.selected"
|
||||
class="issue-card-selected text-center">
|
||||
<i class="fa fa-check"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="group in groupedIssues"
|
||||
class="add-issues-list-column">
|
||||
<div
|
||||
v-for="issue in group"
|
||||
v-if="showIssue(issue)"
|
||||
class="card-parent">
|
||||
<div
|
||||
class="card"
|
||||
:class="{ 'is-active': issue.selected }"
|
||||
@click="toggleIssue($event, issue)">
|
||||
<issue-card-inner
|
||||
:issue="issue"
|
||||
:issue-link-base="issueLinkBase"
|
||||
:root-path="rootPath">
|
||||
</issue-card-inner>
|
||||
<span
|
||||
:aria-label="'Issue #' + issue.id + ' selected'"
|
||||
aria-checked="true"
|
||||
v-if="issue.selected"
|
||||
class="issue-card-selected text-center">
|
||||
<i class="fa fa-check"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,57 +1,55 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
state: gl.issueBoards.BoardsStore.state,
|
||||
};
|
||||
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
state: gl.issueBoards.BoardsStore.state,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selected() {
|
||||
return this.modal.selectedList || this.state.lists[0];
|
||||
},
|
||||
computed: {
|
||||
selected() {
|
||||
return this.modal.selectedList || this.state.lists[0];
|
||||
},
|
||||
},
|
||||
destroyed() {
|
||||
this.modal.selectedList = null;
|
||||
},
|
||||
template: `
|
||||
<div class="dropdown inline">
|
||||
<button
|
||||
class="dropdown-menu-toggle"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<span
|
||||
class="dropdown-label-box"
|
||||
:style="{ backgroundColor: selected.label.color }">
|
||||
</span>
|
||||
{{ selected.title }}
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
|
||||
<ul>
|
||||
<li
|
||||
v-for="list in state.lists"
|
||||
v-if="list.type == 'label'">
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
:class="{ 'is-active': list.id == selected.id }"
|
||||
@click.prevent="modal.selectedList = list">
|
||||
<span
|
||||
class="dropdown-label-box"
|
||||
:style="{ backgroundColor: list.label.color }">
|
||||
</span>
|
||||
{{ list.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
},
|
||||
destroyed() {
|
||||
this.modal.selectedList = null;
|
||||
},
|
||||
template: `
|
||||
<div class="dropdown inline">
|
||||
<button
|
||||
class="dropdown-menu-toggle"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<span
|
||||
class="dropdown-label-box"
|
||||
:style="{ backgroundColor: selected.label.color }">
|
||||
</span>
|
||||
{{ selected.title }}
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
|
||||
<ul>
|
||||
<li
|
||||
v-for="list in state.lists"
|
||||
v-if="list.type == 'label'">
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
:class="{ 'is-active': list.id == selected.id }"
|
||||
@click.prevent="modal.selectedList = list">
|
||||
<span
|
||||
class="dropdown-label-box"
|
||||
:style="{ backgroundColor: list.label.color }">
|
||||
</span>
|
||||
{{ list.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,48 +1,46 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
gl.issueBoards.ModalTabs = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
gl.issueBoards.ModalTabs = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
computed: {
|
||||
selectedCount() {
|
||||
return ModalStore.selectedCount();
|
||||
},
|
||||
computed: {
|
||||
selectedCount() {
|
||||
return ModalStore.selectedCount();
|
||||
},
|
||||
},
|
||||
destroyed() {
|
||||
this.activeTab = 'all';
|
||||
},
|
||||
template: `
|
||||
<div class="top-area prepend-top-10 append-bottom-10">
|
||||
<ul class="nav-links issues-state-filters">
|
||||
<li :class="{ 'active': activeTab == 'all' }">
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="changeTab('all')">
|
||||
Open issues
|
||||
<span class="badge">
|
||||
{{ issuesCount }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li :class="{ 'active': activeTab == 'selected' }">
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="changeTab('selected')">
|
||||
Selected issues
|
||||
<span class="badge">
|
||||
{{ selectedCount }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
},
|
||||
destroyed() {
|
||||
this.activeTab = 'all';
|
||||
},
|
||||
template: `
|
||||
<div class="top-area prepend-top-10 append-bottom-10">
|
||||
<ul class="nav-links issues-state-filters">
|
||||
<li :class="{ 'active': activeTab == 'all' }">
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="changeTab('all')">
|
||||
Open issues
|
||||
<span class="badge">
|
||||
{{ issuesCount }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li :class="{ 'active': activeTab == 'selected' }">
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="changeTab('selected')">
|
||||
Selected issues
|
||||
<span class="badge">
|
||||
{{ selectedCount }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,76 +1,74 @@
|
|||
/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
$(document).off('created.label').on('created.label', (e, label) => {
|
||||
Store.new({
|
||||
$(document).off('created.label').on('created.label', (e, label) => {
|
||||
Store.new({
|
||||
title: label.title,
|
||||
position: Store.state.lists.length - 2,
|
||||
list_type: 'label',
|
||||
label: {
|
||||
id: label.id,
|
||||
title: label.title,
|
||||
position: Store.state.lists.length - 2,
|
||||
list_type: 'label',
|
||||
label: {
|
||||
id: label.id,
|
||||
title: label.title,
|
||||
color: label.color
|
||||
color: label.color
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
gl.issueBoards.newListDropdownInit = () => {
|
||||
$('.js-new-board-list').each(function () {
|
||||
const $this = $(this);
|
||||
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
|
||||
|
||||
$this.glDropdown({
|
||||
data(term, callback) {
|
||||
$.get($this.attr('data-labels'))
|
||||
.then((resp) => {
|
||||
callback(resp);
|
||||
});
|
||||
},
|
||||
renderRow (label) {
|
||||
const active = Store.findList('title', label.title);
|
||||
const $li = $('<li />');
|
||||
const $a = $('<a />', {
|
||||
class: (active ? `is-active js-board-list-${active.id}` : ''),
|
||||
text: label.title,
|
||||
href: '#'
|
||||
});
|
||||
const $labelColor = $('<span />', {
|
||||
class: 'dropdown-label-box',
|
||||
style: `background-color: ${label.color}`
|
||||
});
|
||||
|
||||
return $li.append($a.prepend($labelColor));
|
||||
},
|
||||
search: {
|
||||
fields: ['title']
|
||||
},
|
||||
filterable: true,
|
||||
selectable: true,
|
||||
multiSelect: true,
|
||||
clicked (label, $el, e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!Store.findList('title', label.title)) {
|
||||
Store.new({
|
||||
title: label.title,
|
||||
position: Store.state.lists.length - 2,
|
||||
list_type: 'label',
|
||||
label: {
|
||||
id: label.id,
|
||||
title: label.title,
|
||||
color: label.color
|
||||
}
|
||||
});
|
||||
|
||||
Store.state.lists = _.sortBy(Store.state.lists, 'position');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
gl.issueBoards.newListDropdownInit = () => {
|
||||
$('.js-new-board-list').each(function () {
|
||||
const $this = $(this);
|
||||
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
|
||||
|
||||
$this.glDropdown({
|
||||
data(term, callback) {
|
||||
$.get($this.attr('data-labels'))
|
||||
.then((resp) => {
|
||||
callback(resp);
|
||||
});
|
||||
},
|
||||
renderRow (label) {
|
||||
const active = Store.findList('title', label.title);
|
||||
const $li = $('<li />');
|
||||
const $a = $('<a />', {
|
||||
class: (active ? `is-active js-board-list-${active.id}` : ''),
|
||||
text: label.title,
|
||||
href: '#'
|
||||
});
|
||||
const $labelColor = $('<span />', {
|
||||
class: 'dropdown-label-box',
|
||||
style: `background-color: ${label.color}`
|
||||
});
|
||||
|
||||
return $li.append($a.prepend($labelColor));
|
||||
},
|
||||
search: {
|
||||
fields: ['title']
|
||||
},
|
||||
filterable: true,
|
||||
selectable: true,
|
||||
multiSelect: true,
|
||||
clicked (label, $el, e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!Store.findList('title', label.title)) {
|
||||
Store.new({
|
||||
title: label.title,
|
||||
position: Store.state.lists.length - 2,
|
||||
list_type: 'label',
|
||||
label: {
|
||||
id: label.id,
|
||||
title: label.title,
|
||||
color: label.color
|
||||
}
|
||||
});
|
||||
|
||||
Store.state.lists = _.sortBy(Store.state.lists, 'position');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
})();
|
||||
};
|
||||
|
|
|
@ -3,59 +3,57 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.RemoveIssueBtn = Vue.extend({
|
||||
props: {
|
||||
issue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
list: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
gl.issueBoards.RemoveIssueBtn = Vue.extend({
|
||||
props: {
|
||||
issue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
methods: {
|
||||
removeIssue() {
|
||||
const issue = this.issue;
|
||||
const lists = issue.getLists();
|
||||
const labelIds = lists.map(list => list.label.id);
|
||||
list: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeIssue() {
|
||||
const issue = this.issue;
|
||||
const lists = issue.getLists();
|
||||
const labelIds = lists.map(list => list.label.id);
|
||||
|
||||
// Post the remove data
|
||||
gl.boardService.bulkUpdate([issue.globalId], {
|
||||
remove_label_ids: labelIds,
|
||||
}).catch(() => {
|
||||
new Flash('Failed to remove issue from board, please try again.', 'alert');
|
||||
// Post the remove data
|
||||
gl.boardService.bulkUpdate([issue.globalId], {
|
||||
remove_label_ids: labelIds,
|
||||
}).catch(() => {
|
||||
new Flash('Failed to remove issue from board, please try again.', 'alert');
|
||||
|
||||
lists.forEach((list) => {
|
||||
list.addIssue(issue);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove from the frontend store
|
||||
lists.forEach((list) => {
|
||||
list.removeIssue(issue);
|
||||
list.addIssue(issue);
|
||||
});
|
||||
});
|
||||
|
||||
Store.detail.issue = {};
|
||||
},
|
||||
// Remove from the frontend store
|
||||
lists.forEach((list) => {
|
||||
list.removeIssue(issue);
|
||||
});
|
||||
|
||||
Store.detail.issue = {};
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="block list"
|
||||
v-if="list.type !== 'closed'">
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
type="button"
|
||||
@click="removeIssue">
|
||||
Remove from board
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="block list"
|
||||
v-if="list.type !== 'closed'">
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
type="button"
|
||||
@click="removeIssue">
|
||||
Remove from board
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
(() => {
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
gl.issueBoards.ModalMixins = {
|
||||
methods: {
|
||||
toggleModal(toggle) {
|
||||
ModalStore.store.showAddIssuesModal = toggle;
|
||||
},
|
||||
changeTab(tab) {
|
||||
ModalStore.store.activeTab = tab;
|
||||
},
|
||||
gl.issueBoards.ModalMixins = {
|
||||
methods: {
|
||||
toggleModal(toggle) {
|
||||
ModalStore.store.showAddIssuesModal = toggle;
|
||||
},
|
||||
};
|
||||
})();
|
||||
changeTab(tab) {
|
||||
ModalStore.store.activeTab = tab;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,39 +1,37 @@
|
|||
/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
|
||||
/* global DocumentTouch */
|
||||
|
||||
((w) => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.onStart = () => {
|
||||
$('.has-tooltip').tooltip('hide')
|
||||
.tooltip('disable');
|
||||
document.body.classList.add('is-dragging');
|
||||
gl.issueBoards.onStart = () => {
|
||||
$('.has-tooltip').tooltip('hide')
|
||||
.tooltip('disable');
|
||||
document.body.classList.add('is-dragging');
|
||||
};
|
||||
|
||||
gl.issueBoards.onEnd = () => {
|
||||
$('.has-tooltip').tooltip('enable');
|
||||
document.body.classList.remove('is-dragging');
|
||||
};
|
||||
|
||||
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
|
||||
|
||||
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
|
||||
const defaultSortOptions = {
|
||||
animation: 200,
|
||||
forceFallback: true,
|
||||
fallbackClass: 'is-dragging',
|
||||
fallbackOnBody: true,
|
||||
ghostClass: 'is-ghost',
|
||||
filter: '.board-delete, .btn',
|
||||
delay: gl.issueBoards.touchEnabled ? 100 : 0,
|
||||
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
|
||||
scrollSpeed: 20,
|
||||
onStart: gl.issueBoards.onStart,
|
||||
onEnd: gl.issueBoards.onEnd
|
||||
};
|
||||
|
||||
gl.issueBoards.onEnd = () => {
|
||||
$('.has-tooltip').tooltip('enable');
|
||||
document.body.classList.remove('is-dragging');
|
||||
};
|
||||
|
||||
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
|
||||
|
||||
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
|
||||
const defaultSortOptions = {
|
||||
animation: 200,
|
||||
forceFallback: true,
|
||||
fallbackClass: 'is-dragging',
|
||||
fallbackOnBody: true,
|
||||
ghostClass: 'is-ghost',
|
||||
filter: '.board-delete, .btn',
|
||||
delay: gl.issueBoards.touchEnabled ? 100 : 0,
|
||||
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
|
||||
scrollSpeed: 20,
|
||||
onStart: gl.issueBoards.onStart,
|
||||
onEnd: gl.issueBoards.onEnd
|
||||
};
|
||||
|
||||
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
|
||||
return defaultSortOptions;
|
||||
};
|
||||
})(window);
|
||||
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
|
||||
return defaultSortOptions;
|
||||
};
|
||||
|
|
|
@ -3,125 +3,123 @@
|
|||
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.BoardsStore = {
|
||||
disabled: false,
|
||||
filter: {
|
||||
path: '',
|
||||
},
|
||||
state: {},
|
||||
detail: {
|
||||
issue: {}
|
||||
},
|
||||
moving: {
|
||||
issue: {},
|
||||
list: {}
|
||||
},
|
||||
create () {
|
||||
this.state.lists = [];
|
||||
this.filter.path = gl.utils.getUrlParamsArray().join('&');
|
||||
},
|
||||
addList (listObj) {
|
||||
const list = new List(listObj);
|
||||
this.state.lists.push(list);
|
||||
gl.issueBoards.BoardsStore = {
|
||||
disabled: false,
|
||||
filter: {
|
||||
path: '',
|
||||
},
|
||||
state: {},
|
||||
detail: {
|
||||
issue: {}
|
||||
},
|
||||
moving: {
|
||||
issue: {},
|
||||
list: {}
|
||||
},
|
||||
create () {
|
||||
this.state.lists = [];
|
||||
this.filter.path = gl.utils.getUrlParamsArray().join('&');
|
||||
},
|
||||
addList (listObj) {
|
||||
const list = new List(listObj);
|
||||
this.state.lists.push(list);
|
||||
|
||||
return list;
|
||||
},
|
||||
new (listObj) {
|
||||
const list = this.addList(listObj);
|
||||
return list;
|
||||
},
|
||||
new (listObj) {
|
||||
const list = this.addList(listObj);
|
||||
|
||||
list
|
||||
.save()
|
||||
.then(() => {
|
||||
this.state.lists = _.sortBy(this.state.lists, 'position');
|
||||
});
|
||||
this.removeBlankState();
|
||||
},
|
||||
updateNewListDropdown (listId) {
|
||||
$(`.js-board-list-${listId}`).removeClass('is-active');
|
||||
},
|
||||
shouldAddBlankState () {
|
||||
// Decide whether to add the blank state
|
||||
return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
|
||||
},
|
||||
addBlankState () {
|
||||
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
|
||||
|
||||
this.addList({
|
||||
id: 'blank',
|
||||
list_type: 'blank',
|
||||
title: 'Welcome to your Issue Board!',
|
||||
position: 0
|
||||
list
|
||||
.save()
|
||||
.then(() => {
|
||||
this.state.lists = _.sortBy(this.state.lists, 'position');
|
||||
});
|
||||
this.removeBlankState();
|
||||
},
|
||||
updateNewListDropdown (listId) {
|
||||
$(`.js-board-list-${listId}`).removeClass('is-active');
|
||||
},
|
||||
shouldAddBlankState () {
|
||||
// Decide whether to add the blank state
|
||||
return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
|
||||
},
|
||||
addBlankState () {
|
||||
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
|
||||
|
||||
this.state.lists = _.sortBy(this.state.lists, 'position');
|
||||
},
|
||||
removeBlankState () {
|
||||
this.removeList('blank');
|
||||
this.addList({
|
||||
id: 'blank',
|
||||
list_type: 'blank',
|
||||
title: 'Welcome to your Issue Board!',
|
||||
position: 0
|
||||
});
|
||||
|
||||
Cookies.set('issue_board_welcome_hidden', 'true', {
|
||||
expires: 365 * 10,
|
||||
path: ''
|
||||
});
|
||||
},
|
||||
welcomeIsHidden () {
|
||||
return Cookies.get('issue_board_welcome_hidden') === 'true';
|
||||
},
|
||||
removeList (id, type = 'blank') {
|
||||
const list = this.findList('id', id, type);
|
||||
this.state.lists = _.sortBy(this.state.lists, 'position');
|
||||
},
|
||||
removeBlankState () {
|
||||
this.removeList('blank');
|
||||
|
||||
if (!list) return;
|
||||
Cookies.set('issue_board_welcome_hidden', 'true', {
|
||||
expires: 365 * 10,
|
||||
path: ''
|
||||
});
|
||||
},
|
||||
welcomeIsHidden () {
|
||||
return Cookies.get('issue_board_welcome_hidden') === 'true';
|
||||
},
|
||||
removeList (id, type = 'blank') {
|
||||
const list = this.findList('id', id, type);
|
||||
|
||||
this.state.lists = this.state.lists.filter(list => list.id !== id);
|
||||
},
|
||||
moveList (listFrom, orderLists) {
|
||||
orderLists.forEach((id, i) => {
|
||||
const list = this.findList('id', parseInt(id, 10));
|
||||
if (!list) return;
|
||||
|
||||
list.position = i;
|
||||
});
|
||||
listFrom.update();
|
||||
},
|
||||
moveIssueToList (listFrom, listTo, issue, newIndex) {
|
||||
const issueTo = listTo.findIssue(issue.id);
|
||||
const issueLists = issue.getLists();
|
||||
const listLabels = issueLists.map(listIssue => listIssue.label);
|
||||
this.state.lists = this.state.lists.filter(list => list.id !== id);
|
||||
},
|
||||
moveList (listFrom, orderLists) {
|
||||
orderLists.forEach((id, i) => {
|
||||
const list = this.findList('id', parseInt(id, 10));
|
||||
|
||||
if (!issueTo) {
|
||||
// Add to new lists issues if it doesn't already exist
|
||||
listTo.addIssue(issue, listFrom, newIndex);
|
||||
} else {
|
||||
listTo.updateIssueLabel(issue, listFrom);
|
||||
issueTo.removeLabel(listFrom.label);
|
||||
}
|
||||
list.position = i;
|
||||
});
|
||||
listFrom.update();
|
||||
},
|
||||
moveIssueToList (listFrom, listTo, issue, newIndex) {
|
||||
const issueTo = listTo.findIssue(issue.id);
|
||||
const issueLists = issue.getLists();
|
||||
const listLabels = issueLists.map(listIssue => listIssue.label);
|
||||
|
||||
if (listTo.type === 'closed') {
|
||||
issueLists.forEach((list) => {
|
||||
list.removeIssue(issue);
|
||||
});
|
||||
issue.removeLabels(listLabels);
|
||||
} else {
|
||||
listFrom.removeIssue(issue);
|
||||
}
|
||||
},
|
||||
moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
|
||||
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
|
||||
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
|
||||
|
||||
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
|
||||
},
|
||||
findList (key, val, type = 'label') {
|
||||
return this.state.lists.filter((list) => {
|
||||
const byType = type ? list['type'] === type : true;
|
||||
|
||||
return list[key] === val && byType;
|
||||
})[0];
|
||||
},
|
||||
updateFiltersUrl () {
|
||||
history.pushState(null, null, `?${this.filter.path}`);
|
||||
if (!issueTo) {
|
||||
// Add to new lists issues if it doesn't already exist
|
||||
listTo.addIssue(issue, listFrom, newIndex);
|
||||
} else {
|
||||
listTo.updateIssueLabel(issue, listFrom);
|
||||
issueTo.removeLabel(listFrom.label);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
if (listTo.type === 'closed') {
|
||||
issueLists.forEach((list) => {
|
||||
list.removeIssue(issue);
|
||||
});
|
||||
issue.removeLabels(listLabels);
|
||||
} else {
|
||||
listFrom.removeIssue(issue);
|
||||
}
|
||||
},
|
||||
moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
|
||||
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
|
||||
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
|
||||
|
||||
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
|
||||
},
|
||||
findList (key, val, type = 'label') {
|
||||
return this.state.lists.filter((list) => {
|
||||
const byType = type ? list['type'] === type : true;
|
||||
|
||||
return list[key] === val && byType;
|
||||
})[0];
|
||||
},
|
||||
updateFiltersUrl () {
|
||||
history.pushState(null, null, `?${this.filter.path}`);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,100 +1,98 @@
|
|||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
class ModalStore {
|
||||
constructor() {
|
||||
this.store = {
|
||||
columns: 3,
|
||||
issues: [],
|
||||
issuesCount: false,
|
||||
selectedIssues: [],
|
||||
showAddIssuesModal: false,
|
||||
activeTab: 'all',
|
||||
selectedList: null,
|
||||
searchTerm: '',
|
||||
loading: false,
|
||||
loadingNewPage: false,
|
||||
filterLoading: false,
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
filter: {
|
||||
path: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
class ModalStore {
|
||||
constructor() {
|
||||
this.store = {
|
||||
columns: 3,
|
||||
issues: [],
|
||||
issuesCount: false,
|
||||
selectedIssues: [],
|
||||
showAddIssuesModal: false,
|
||||
activeTab: 'all',
|
||||
selectedList: null,
|
||||
searchTerm: '',
|
||||
loading: false,
|
||||
loadingNewPage: false,
|
||||
filterLoading: false,
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
filter: {
|
||||
path: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
selectedCount() {
|
||||
return this.getSelectedIssues().length;
|
||||
}
|
||||
selectedCount() {
|
||||
return this.getSelectedIssues().length;
|
||||
}
|
||||
|
||||
toggleIssue(issueObj) {
|
||||
const issue = issueObj;
|
||||
const selected = issue.selected;
|
||||
toggleIssue(issueObj) {
|
||||
const issue = issueObj;
|
||||
const selected = issue.selected;
|
||||
|
||||
issue.selected = !selected;
|
||||
issue.selected = !selected;
|
||||
|
||||
if (!selected) {
|
||||
this.addSelectedIssue(issue);
|
||||
} else {
|
||||
this.removeSelectedIssue(issue);
|
||||
}
|
||||
}
|
||||
|
||||
toggleAll() {
|
||||
const select = this.selectedCount() !== this.store.issues.length;
|
||||
|
||||
this.store.issues.forEach((issue) => {
|
||||
const issueUpdate = issue;
|
||||
|
||||
if (issueUpdate.selected !== select) {
|
||||
issueUpdate.selected = select;
|
||||
|
||||
if (select) {
|
||||
this.addSelectedIssue(issue);
|
||||
} else {
|
||||
this.removeSelectedIssue(issue);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedIssues() {
|
||||
return this.store.selectedIssues.filter(issue => issue.selected);
|
||||
}
|
||||
|
||||
addSelectedIssue(issue) {
|
||||
const index = this.selectedIssueIndex(issue);
|
||||
|
||||
if (index === -1) {
|
||||
this.store.selectedIssues.push(issue);
|
||||
}
|
||||
}
|
||||
|
||||
removeSelectedIssue(issue, forcePurge = false) {
|
||||
if (this.store.activeTab === 'all' || forcePurge) {
|
||||
this.store.selectedIssues = this.store.selectedIssues
|
||||
.filter(fIssue => fIssue.id !== issue.id);
|
||||
}
|
||||
}
|
||||
|
||||
purgeUnselectedIssues() {
|
||||
this.store.selectedIssues.forEach((issue) => {
|
||||
if (!issue.selected) {
|
||||
this.removeSelectedIssue(issue, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectedIssueIndex(issue) {
|
||||
return this.store.selectedIssues.indexOf(issue);
|
||||
}
|
||||
|
||||
findSelectedIssue(issue) {
|
||||
return this.store.selectedIssues
|
||||
.filter(filteredIssue => filteredIssue.id === issue.id)[0];
|
||||
if (!selected) {
|
||||
this.addSelectedIssue(issue);
|
||||
} else {
|
||||
this.removeSelectedIssue(issue);
|
||||
}
|
||||
}
|
||||
|
||||
gl.issueBoards.ModalStore = new ModalStore();
|
||||
})();
|
||||
toggleAll() {
|
||||
const select = this.selectedCount() !== this.store.issues.length;
|
||||
|
||||
this.store.issues.forEach((issue) => {
|
||||
const issueUpdate = issue;
|
||||
|
||||
if (issueUpdate.selected !== select) {
|
||||
issueUpdate.selected = select;
|
||||
|
||||
if (select) {
|
||||
this.addSelectedIssue(issue);
|
||||
} else {
|
||||
this.removeSelectedIssue(issue);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedIssues() {
|
||||
return this.store.selectedIssues.filter(issue => issue.selected);
|
||||
}
|
||||
|
||||
addSelectedIssue(issue) {
|
||||
const index = this.selectedIssueIndex(issue);
|
||||
|
||||
if (index === -1) {
|
||||
this.store.selectedIssues.push(issue);
|
||||
}
|
||||
}
|
||||
|
||||
removeSelectedIssue(issue, forcePurge = false) {
|
||||
if (this.store.activeTab === 'all' || forcePurge) {
|
||||
this.store.selectedIssues = this.store.selectedIssues
|
||||
.filter(fIssue => fIssue.id !== issue.id);
|
||||
}
|
||||
}
|
||||
|
||||
purgeUnselectedIssues() {
|
||||
this.store.selectedIssues.forEach((issue) => {
|
||||
if (!issue.selected) {
|
||||
this.removeSelectedIssue(issue, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectedIssueIndex(issue) {
|
||||
return this.store.selectedIssues.indexOf(issue);
|
||||
}
|
||||
|
||||
findSelectedIssue(issue) {
|
||||
return this.store.selectedIssues
|
||||
.filter(filteredIssue => filteredIssue.id === issue.id)[0];
|
||||
}
|
||||
}
|
||||
|
||||
gl.issueBoards.ModalStore = new ModalStore();
|
||||
|
|
|
@ -2,46 +2,45 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
|
||||
((global) => {
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
const global = window.gl || (window.gl = {});
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
global.cycleAnalytics.StageCodeComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="mergeRequest in items" class="stage-event-item">
|
||||
<div class="item-details">
|
||||
<img class="avatar" :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>
|
||||
·
|
||||
<span>
|
||||
Opened
|
||||
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
|
||||
</span>
|
||||
<span>
|
||||
by
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
global.cycleAnalytics.StageCodeComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="mergeRequest in items" class="stage-event-item">
|
||||
<div class="item-details">
|
||||
<img class="avatar" :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>
|
||||
·
|
||||
<span>
|
||||
Opened
|
||||
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
|
||||
</span>
|
||||
<span>
|
||||
by
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -2,48 +2,47 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
|
||||
((global) => {
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
const global = window.gl || (window.gl = {});
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
global.cycleAnalytics.StageIssueComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="issue in items" class="stage-event-item">
|
||||
<div class="item-details">
|
||||
<img class="avatar" :src="issue.author.avatarUrl">
|
||||
<h5 class="item-title issue-title">
|
||||
<a class="issue-title" :href="issue.url">
|
||||
{{ issue.title }}
|
||||
</a>
|
||||
</h5>
|
||||
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
|
||||
·
|
||||
<span>
|
||||
Opened
|
||||
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
|
||||
</span>
|
||||
<span>
|
||||
by
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
global.cycleAnalytics.StageIssueComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="issue in items" class="stage-event-item">
|
||||
<div class="item-details">
|
||||
<img class="avatar" :src="issue.author.avatarUrl">
|
||||
<h5 class="item-title issue-title">
|
||||
<a class="issue-title" :href="issue.url">
|
||||
{{ issue.title }}
|
||||
</a>
|
||||
</h5>
|
||||
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
|
||||
·
|
||||
<span>
|
||||
Opened
|
||||
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
|
||||
</span>
|
||||
<span>
|
||||
by
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -2,50 +2,49 @@
|
|||
import Vue from 'vue';
|
||||
import iconCommit from '../svg/icon_commit.svg';
|
||||
|
||||
((global) => {
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
const global = window.gl || (window.gl = {});
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
global.cycleAnalytics.StagePlanComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
global.cycleAnalytics.StagePlanComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
|
||||
data() {
|
||||
return { iconCommit };
|
||||
},
|
||||
data() {
|
||||
return { iconCommit };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="commit in items" class="stage-event-item">
|
||||
<div class="item-details item-conmmit-component">
|
||||
<img class="avatar" :src="commit.author.avatarUrl">
|
||||
<h5 class="item-title commit-title">
|
||||
<a :href="commit.commitUrl">
|
||||
{{ commit.title }}
|
||||
</a>
|
||||
</h5>
|
||||
<span>
|
||||
First
|
||||
<span class="commit-icon">${iconCommit}</span>
|
||||
<a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
|
||||
pushed by
|
||||
<a :href="commit.author.webUrl" class="commit-author-link">
|
||||
{{ commit.author.name }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="item-time">
|
||||
<total-time :time="commit.totalTime"></total-time>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="commit in items" class="stage-event-item">
|
||||
<div class="item-details item-conmmit-component">
|
||||
<img class="avatar" :src="commit.author.avatarUrl">
|
||||
<h5 class="item-title commit-title">
|
||||
<a :href="commit.commitUrl">
|
||||
{{ commit.title }}
|
||||
</a>
|
||||
</h5>
|
||||
<span>
|
||||
First
|
||||
<span class="commit-icon">${iconCommit}</span>
|
||||
<a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
|
||||
pushed by
|
||||
<a :href="commit.author.webUrl" class="commit-author-link">
|
||||
{{ commit.author.name }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="item-time">
|
||||
<total-time :time="commit.totalTime"></total-time>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -2,48 +2,47 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
|
||||
((global) => {
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
const global = window.gl || (window.gl = {});
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
global.cycleAnalytics.StageProductionComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="issue in items" class="stage-event-item">
|
||||
<div class="item-details">
|
||||
<img class="avatar" :src="issue.author.avatarUrl">
|
||||
<h5 class="item-title issue-title">
|
||||
<a class="issue-title" :href="issue.url">
|
||||
{{ issue.title }}
|
||||
</a>
|
||||
</h5>
|
||||
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
|
||||
·
|
||||
<span>
|
||||
Opened
|
||||
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
|
||||
</span>
|
||||
<span>
|
||||
by
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
global.cycleAnalytics.StageProductionComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="issue in items" class="stage-event-item">
|
||||
<div class="item-details">
|
||||
<img class="avatar" :src="issue.author.avatarUrl">
|
||||
<h5 class="item-title issue-title">
|
||||
<a class="issue-title" :href="issue.url">
|
||||
{{ issue.title }}
|
||||
</a>
|
||||
</h5>
|
||||
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
|
||||
·
|
||||
<span>
|
||||
Opened
|
||||
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
|
||||
</span>
|
||||
<span>
|
||||
by
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -2,58 +2,57 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
|
||||
((global) => {
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
const global = window.gl || (window.gl = {});
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
global.cycleAnalytics.StageReviewComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="mergeRequest in items" class="stage-event-item">
|
||||
<div class="item-details">
|
||||
<img class="avatar" :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>
|
||||
·
|
||||
<span>
|
||||
Opened
|
||||
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
|
||||
</span>
|
||||
<span>
|
||||
by
|
||||
<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>
|
||||
{{ mergeRequest.state.toUpperCase() }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="merge-request-branch" v-if="mergeRequest.branch">
|
||||
<i class= "fa fa-code-fork"></i>
|
||||
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="item-time">
|
||||
<total-time :time="mergeRequest.totalTime"></total-time>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
global.cycleAnalytics.StageReviewComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="mergeRequest in items" class="stage-event-item">
|
||||
<div class="item-details">
|
||||
<img class="avatar" :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>
|
||||
·
|
||||
<span>
|
||||
Opened
|
||||
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
|
||||
</span>
|
||||
<span>
|
||||
by
|
||||
<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>
|
||||
{{ mergeRequest.state.toUpperCase() }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="merge-request-branch" v-if="mergeRequest.branch">
|
||||
<i class= "fa fa-code-fork"></i>
|
||||
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="item-time">
|
||||
<total-time :time="mergeRequest.totalTime"></total-time>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -2,48 +2,47 @@
|
|||
import Vue from 'vue';
|
||||
import iconBranch from '../svg/icon_branch.svg';
|
||||
|
||||
((global) => {
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
const global = window.gl || (window.gl = {});
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
global.cycleAnalytics.StageStagingComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
data() {
|
||||
return { iconBranch };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="build in items" class="stage-event-item item-build-component">
|
||||
<div class="item-details">
|
||||
<img class="avatar" :src="build.author.avatarUrl">
|
||||
<h5 class="item-title">
|
||||
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
|
||||
<i class="fa fa-code-fork"></i>
|
||||
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
|
||||
<span class="icon-branch">${iconBranch}</span>
|
||||
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
|
||||
</h5>
|
||||
<span>
|
||||
<a :href="build.url" class="build-date">{{ build.date }}</a>
|
||||
by
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
global.cycleAnalytics.StageStagingComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
data() {
|
||||
return { iconBranch };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="build in items" class="stage-event-item item-build-component">
|
||||
<div class="item-details">
|
||||
<img class="avatar" :src="build.author.avatarUrl">
|
||||
<h5 class="item-title">
|
||||
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
|
||||
<i class="fa fa-code-fork"></i>
|
||||
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
|
||||
<span class="icon-branch">${iconBranch}</span>
|
||||
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
|
||||
</h5>
|
||||
<span>
|
||||
<a :href="build.url" class="build-date">{{ build.date }}</a>
|
||||
by
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -3,48 +3,47 @@ import Vue from 'vue';
|
|||
import iconBuildStatus from '../svg/icon_build_status.svg';
|
||||
import iconBranch from '../svg/icon_branch.svg';
|
||||
|
||||
((global) => {
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
const global = window.gl || (window.gl = {});
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
global.cycleAnalytics.StageTestComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
data() {
|
||||
return { iconBuildStatus, iconBranch };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="build in items" class="stage-event-item item-build-component">
|
||||
<div class="item-details">
|
||||
<h5 class="item-title">
|
||||
<span class="icon-build-status">${iconBuildStatus}</span>
|
||||
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
|
||||
·
|
||||
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
|
||||
<i class="fa fa-code-fork"></i>
|
||||
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
|
||||
<span class="icon-branch">${iconBranch}</span>
|
||||
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
|
||||
</h5>
|
||||
<span>
|
||||
<a :href="build.url" class="issue-date">
|
||||
{{ build.date }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="item-time">
|
||||
<total-time :time="build.totalTime"></total-time>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
global.cycleAnalytics.StageTestComponent = Vue.extend({
|
||||
props: {
|
||||
items: Array,
|
||||
stage: Object,
|
||||
},
|
||||
data() {
|
||||
return { iconBuildStatus, iconBranch };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<limit-warning :count="items.length" />
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="build in items" class="stage-event-item item-build-component">
|
||||
<div class="item-details">
|
||||
<h5 class="item-title">
|
||||
<span class="icon-build-status">${iconBuildStatus}</span>
|
||||
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
|
||||
·
|
||||
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
|
||||
<i class="fa fa-code-fork"></i>
|
||||
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
|
||||
<span class="icon-branch">${iconBranch}</span>
|
||||
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
|
||||
</h5>
|
||||
<span>
|
||||
<a :href="build.url" class="issue-date">
|
||||
{{ build.date }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="item-time">
|
||||
<total-time :time="build.totalTime"></total-time>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -2,25 +2,24 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
|
||||
((global) => {
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
const global = window.gl || (window.gl = {});
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
global.cycleAnalytics.TotalTimeComponent = Vue.extend({
|
||||
props: {
|
||||
time: Object,
|
||||
},
|
||||
template: `
|
||||
<span class="total-time">
|
||||
<template v-if="Object.keys(time).length">
|
||||
<template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
|
||||
<template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
|
||||
<template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
|
||||
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
|
||||
</template>
|
||||
<template v-else>
|
||||
--
|
||||
</template>
|
||||
</span>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
||||
global.cycleAnalytics.TotalTimeComponent = Vue.extend({
|
||||
props: {
|
||||
time: Object,
|
||||
},
|
||||
template: `
|
||||
<span class="total-time">
|
||||
<template v-if="Object.keys(time).length">
|
||||
<template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
|
||||
<template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
|
||||
<template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
|
||||
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
|
||||
</template>
|
||||
<template v-else>
|
||||
--
|
||||
</template>
|
||||
</span>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,41 +1,41 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
((global) => {
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
class CycleAnalyticsService {
|
||||
constructor(options) {
|
||||
this.requestPath = options.requestPath;
|
||||
}
|
||||
const global = window.gl || (window.gl = {});
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
fetchCycleAnalyticsData(options) {
|
||||
options = options || { startDate: 30 };
|
||||
|
||||
return $.ajax({
|
||||
url: this.requestPath,
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
data: {
|
||||
cycle_analytics: {
|
||||
start_date: options.startDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fetchStageData(options) {
|
||||
const {
|
||||
stage,
|
||||
startDate,
|
||||
} = options;
|
||||
|
||||
return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
|
||||
cycle_analytics: {
|
||||
start_date: startDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
class CycleAnalyticsService {
|
||||
constructor(options) {
|
||||
this.requestPath = options.requestPath;
|
||||
}
|
||||
|
||||
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
|
||||
})(window.gl || (window.gl = {}));
|
||||
fetchCycleAnalyticsData(options) {
|
||||
options = options || { startDate: 30 };
|
||||
|
||||
return $.ajax({
|
||||
url: this.requestPath,
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
data: {
|
||||
cycle_analytics: {
|
||||
start_date: options.startDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fetchStageData(options) {
|
||||
const {
|
||||
stage,
|
||||
startDate,
|
||||
} = options;
|
||||
|
||||
return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
|
||||
cycle_analytics: {
|
||||
start_date: startDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
|
||||
|
|
|
@ -3,102 +3,101 @@
|
|||
require('../lib/utils/text_utility');
|
||||
const DEFAULT_EVENT_OBJECTS = require('./default_event_objects');
|
||||
|
||||
((global) => {
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
const global = window.gl || (window.gl = {});
|
||||
global.cycleAnalytics = global.cycleAnalytics || {};
|
||||
|
||||
const EMPTY_STAGE_TEXTS = {
|
||||
issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
|
||||
plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
|
||||
code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
|
||||
test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
|
||||
review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
|
||||
staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
|
||||
production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
|
||||
};
|
||||
const EMPTY_STAGE_TEXTS = {
|
||||
issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
|
||||
plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
|
||||
code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
|
||||
test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
|
||||
review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
|
||||
staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
|
||||
production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
|
||||
};
|
||||
|
||||
global.cycleAnalytics.CycleAnalyticsStore = {
|
||||
state: {
|
||||
summary: '',
|
||||
stats: '',
|
||||
analytics: '',
|
||||
events: [],
|
||||
stages: [],
|
||||
},
|
||||
setCycleAnalyticsData(data) {
|
||||
this.state = Object.assign(this.state, this.decorateData(data));
|
||||
},
|
||||
decorateData(data) {
|
||||
const newData = {};
|
||||
global.cycleAnalytics.CycleAnalyticsStore = {
|
||||
state: {
|
||||
summary: '',
|
||||
stats: '',
|
||||
analytics: '',
|
||||
events: [],
|
||||
stages: [],
|
||||
},
|
||||
setCycleAnalyticsData(data) {
|
||||
this.state = Object.assign(this.state, this.decorateData(data));
|
||||
},
|
||||
decorateData(data) {
|
||||
const newData = {};
|
||||
|
||||
newData.stages = data.stats || [];
|
||||
newData.summary = data.summary || [];
|
||||
newData.stages = data.stats || [];
|
||||
newData.summary = data.summary || [];
|
||||
|
||||
newData.summary.forEach((item) => {
|
||||
item.value = item.value || '-';
|
||||
});
|
||||
newData.summary.forEach((item) => {
|
||||
item.value = item.value || '-';
|
||||
});
|
||||
|
||||
newData.stages.forEach((item) => {
|
||||
const stageSlug = gl.text.dasherize(item.title.toLowerCase());
|
||||
item.active = false;
|
||||
item.isUserAllowed = data.permissions[stageSlug];
|
||||
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
|
||||
item.component = `stage-${stageSlug}-component`;
|
||||
item.slug = stageSlug;
|
||||
});
|
||||
newData.analytics = data;
|
||||
return newData;
|
||||
},
|
||||
setLoadingState(state) {
|
||||
this.state.isLoading = state;
|
||||
},
|
||||
setErrorState(state) {
|
||||
this.state.hasError = state;
|
||||
},
|
||||
deactivateAllStages() {
|
||||
this.state.stages.forEach((stage) => {
|
||||
stage.active = false;
|
||||
});
|
||||
},
|
||||
setActiveStage(stage) {
|
||||
this.deactivateAllStages();
|
||||
stage.active = true;
|
||||
},
|
||||
setStageEvents(events, stage) {
|
||||
this.state.events = this.decorateEvents(events, stage);
|
||||
},
|
||||
decorateEvents(events, stage) {
|
||||
const newEvents = [];
|
||||
newData.stages.forEach((item) => {
|
||||
const stageSlug = gl.text.dasherize(item.title.toLowerCase());
|
||||
item.active = false;
|
||||
item.isUserAllowed = data.permissions[stageSlug];
|
||||
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
|
||||
item.component = `stage-${stageSlug}-component`;
|
||||
item.slug = stageSlug;
|
||||
});
|
||||
newData.analytics = data;
|
||||
return newData;
|
||||
},
|
||||
setLoadingState(state) {
|
||||
this.state.isLoading = state;
|
||||
},
|
||||
setErrorState(state) {
|
||||
this.state.hasError = state;
|
||||
},
|
||||
deactivateAllStages() {
|
||||
this.state.stages.forEach((stage) => {
|
||||
stage.active = false;
|
||||
});
|
||||
},
|
||||
setActiveStage(stage) {
|
||||
this.deactivateAllStages();
|
||||
stage.active = true;
|
||||
},
|
||||
setStageEvents(events, stage) {
|
||||
this.state.events = this.decorateEvents(events, stage);
|
||||
},
|
||||
decorateEvents(events, stage) {
|
||||
const newEvents = [];
|
||||
|
||||
events.forEach((item) => {
|
||||
if (!item) return;
|
||||
events.forEach((item) => {
|
||||
if (!item) return;
|
||||
|
||||
const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
|
||||
const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
|
||||
|
||||
eventItem.totalTime = eventItem.total_time;
|
||||
eventItem.totalTime = eventItem.total_time;
|
||||
|
||||
if (eventItem.author) {
|
||||
eventItem.author.webUrl = eventItem.author.web_url;
|
||||
eventItem.author.avatarUrl = eventItem.author.avatar_url;
|
||||
}
|
||||
if (eventItem.author) {
|
||||
eventItem.author.webUrl = eventItem.author.web_url;
|
||||
eventItem.author.avatarUrl = eventItem.author.avatar_url;
|
||||
}
|
||||
|
||||
if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
|
||||
if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
|
||||
if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url;
|
||||
if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
|
||||
if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
|
||||
if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url;
|
||||
|
||||
delete eventItem.author.web_url;
|
||||
delete eventItem.author.avatar_url;
|
||||
delete eventItem.total_time;
|
||||
delete eventItem.created_at;
|
||||
delete eventItem.short_sha;
|
||||
delete eventItem.commit_url;
|
||||
delete eventItem.author.web_url;
|
||||
delete eventItem.author.avatar_url;
|
||||
delete eventItem.total_time;
|
||||
delete eventItem.created_at;
|
||||
delete eventItem.short_sha;
|
||||
delete eventItem.commit_url;
|
||||
|
||||
newEvents.push(eventItem);
|
||||
});
|
||||
newEvents.push(eventItem);
|
||||
});
|
||||
|
||||
return newEvents;
|
||||
},
|
||||
currentActiveStage() {
|
||||
return this.state.stages.find(stage => stage.active);
|
||||
},
|
||||
};
|
||||
})(window.gl || (window.gl = {}));
|
||||
return newEvents;
|
||||
},
|
||||
currentActiveStage() {
|
||||
return this.state.stages.find(stage => stage.active);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,192 +1,188 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */
|
||||
|
||||
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
|
||||
require('vendor/latinise');
|
||||
|
||||
(function() {
|
||||
(function(w) {
|
||||
var base;
|
||||
if (w.gl == null) {
|
||||
w.gl = {};
|
||||
var base;
|
||||
var w = window;
|
||||
if (w.gl == null) {
|
||||
w.gl = {};
|
||||
}
|
||||
if ((base = w.gl).text == null) {
|
||||
base.text = {};
|
||||
}
|
||||
gl.text.addDelimiter = function(text) {
|
||||
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
|
||||
};
|
||||
gl.text.highCountTrim = function(count) {
|
||||
return count > 99 ? '99+' : count;
|
||||
};
|
||||
gl.text.randomString = function() {
|
||||
return Math.random().toString(36).substring(7);
|
||||
};
|
||||
gl.text.replaceRange = function(s, start, end, substitute) {
|
||||
return s.substring(0, start) + substitute + s.substring(end);
|
||||
};
|
||||
gl.text.getTextWidth = function(text, font) {
|
||||
/**
|
||||
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
|
||||
*
|
||||
* @param {String} text The text to be rendered.
|
||||
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
|
||||
*
|
||||
* @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
||||
*/
|
||||
// re-use canvas object for better performance
|
||||
var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
|
||||
var context = canvas.getContext('2d');
|
||||
context.font = font;
|
||||
return context.measureText(text).width;
|
||||
};
|
||||
gl.text.selectedText = function(text, textarea) {
|
||||
return text.substring(textarea.selectionStart, textarea.selectionEnd);
|
||||
};
|
||||
gl.text.lineBefore = function(text, textarea) {
|
||||
var split;
|
||||
split = text.substring(0, textarea.selectionStart).trim().split('\n');
|
||||
return split[split.length - 1];
|
||||
};
|
||||
gl.text.lineAfter = function(text, textarea) {
|
||||
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
|
||||
};
|
||||
gl.text.blockTagText = function(text, textArea, blockTag, selected) {
|
||||
var lineAfter, lineBefore;
|
||||
lineBefore = this.lineBefore(text, textArea);
|
||||
lineAfter = this.lineAfter(text, textArea);
|
||||
if (lineBefore === blockTag && lineAfter === blockTag) {
|
||||
// To remove the block tag we have to select the line before & after
|
||||
if (blockTag != null) {
|
||||
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
|
||||
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
|
||||
}
|
||||
if ((base = w.gl).text == null) {
|
||||
base.text = {};
|
||||
return selected;
|
||||
} else {
|
||||
return blockTag + "\n" + selected + "\n" + blockTag;
|
||||
}
|
||||
};
|
||||
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
|
||||
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
|
||||
removedLastNewLine = false;
|
||||
removedFirstNewLine = false;
|
||||
currentLineEmpty = false;
|
||||
|
||||
// Remove the first newline
|
||||
if (selected.indexOf('\n') === 0) {
|
||||
removedFirstNewLine = true;
|
||||
selected = selected.replace(/\n+/, '');
|
||||
}
|
||||
|
||||
// Remove the last newline
|
||||
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
|
||||
removedLastNewLine = true;
|
||||
selected = selected.replace(/\n$/, '');
|
||||
}
|
||||
|
||||
selectedSplit = selected.split('\n');
|
||||
|
||||
if (!wrap) {
|
||||
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
|
||||
|
||||
// Check whether the current line is empty or consists only of spaces(=handle as empty)
|
||||
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
|
||||
currentLineEmpty = true;
|
||||
}
|
||||
gl.text.addDelimiter = function(text) {
|
||||
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
|
||||
};
|
||||
gl.text.highCountTrim = function(count) {
|
||||
return count > 99 ? '99+' : count;
|
||||
};
|
||||
gl.text.randomString = function() {
|
||||
return Math.random().toString(36).substring(7);
|
||||
};
|
||||
gl.text.replaceRange = function(s, start, end, substitute) {
|
||||
return s.substring(0, start) + substitute + s.substring(end);
|
||||
};
|
||||
gl.text.getTextWidth = function(text, font) {
|
||||
/**
|
||||
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
|
||||
*
|
||||
* @param {String} text The text to be rendered.
|
||||
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
|
||||
*
|
||||
* @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
||||
*/
|
||||
// re-use canvas object for better performance
|
||||
var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
|
||||
var context = canvas.getContext('2d');
|
||||
context.font = font;
|
||||
return context.measureText(text).width;
|
||||
};
|
||||
gl.text.selectedText = function(text, textarea) {
|
||||
return text.substring(textarea.selectionStart, textarea.selectionEnd);
|
||||
};
|
||||
gl.text.lineBefore = function(text, textarea) {
|
||||
var split;
|
||||
split = text.substring(0, textarea.selectionStart).trim().split('\n');
|
||||
return split[split.length - 1];
|
||||
};
|
||||
gl.text.lineAfter = function(text, textarea) {
|
||||
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
|
||||
};
|
||||
gl.text.blockTagText = function(text, textArea, blockTag, selected) {
|
||||
var lineAfter, lineBefore;
|
||||
lineBefore = this.lineBefore(text, textArea);
|
||||
lineAfter = this.lineAfter(text, textArea);
|
||||
if (lineBefore === blockTag && lineAfter === blockTag) {
|
||||
// To remove the block tag we have to select the line before & after
|
||||
if (blockTag != null) {
|
||||
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
|
||||
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
|
||||
}
|
||||
return selected;
|
||||
} else {
|
||||
return blockTag + "\n" + selected + "\n" + blockTag;
|
||||
}
|
||||
};
|
||||
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
|
||||
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
|
||||
removedLastNewLine = false;
|
||||
removedFirstNewLine = false;
|
||||
currentLineEmpty = false;
|
||||
}
|
||||
|
||||
// Remove the first newline
|
||||
if (selected.indexOf('\n') === 0) {
|
||||
removedFirstNewLine = true;
|
||||
selected = selected.replace(/\n+/, '');
|
||||
}
|
||||
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
|
||||
|
||||
// Remove the last newline
|
||||
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
|
||||
removedLastNewLine = true;
|
||||
selected = selected.replace(/\n$/, '');
|
||||
}
|
||||
|
||||
selectedSplit = selected.split('\n');
|
||||
|
||||
if (!wrap) {
|
||||
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
|
||||
|
||||
// Check whether the current line is empty or consists only of spaces(=handle as empty)
|
||||
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
|
||||
currentLineEmpty = true;
|
||||
}
|
||||
}
|
||||
|
||||
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
|
||||
|
||||
if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
|
||||
if (blockTag != null) {
|
||||
insertText = this.blockTagText(text, textArea, blockTag, selected);
|
||||
if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
|
||||
if (blockTag != null) {
|
||||
insertText = this.blockTagText(text, textArea, blockTag, selected);
|
||||
} else {
|
||||
insertText = selectedSplit.map(function(val) {
|
||||
if (val.indexOf(tag) === 0) {
|
||||
return "" + (val.replace(tag, ''));
|
||||
} else {
|
||||
insertText = selectedSplit.map(function(val) {
|
||||
if (val.indexOf(tag) === 0) {
|
||||
return "" + (val.replace(tag, ''));
|
||||
} else {
|
||||
return "" + tag + val;
|
||||
}
|
||||
}).join('\n');
|
||||
return "" + tag + val;
|
||||
}
|
||||
} else {
|
||||
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
|
||||
}
|
||||
}).join('\n');
|
||||
}
|
||||
} else {
|
||||
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
|
||||
}
|
||||
|
||||
if (removedFirstNewLine) {
|
||||
insertText = '\n' + insertText;
|
||||
}
|
||||
if (removedFirstNewLine) {
|
||||
insertText = '\n' + insertText;
|
||||
}
|
||||
|
||||
if (removedLastNewLine) {
|
||||
insertText += '\n';
|
||||
}
|
||||
if (removedLastNewLine) {
|
||||
insertText += '\n';
|
||||
}
|
||||
|
||||
if (document.queryCommandSupported('insertText')) {
|
||||
inserted = document.execCommand('insertText', false, insertText);
|
||||
}
|
||||
if (!inserted) {
|
||||
try {
|
||||
document.execCommand("ms-beginUndoUnit");
|
||||
} catch (error) {}
|
||||
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
|
||||
try {
|
||||
document.execCommand("ms-endUndoUnit");
|
||||
} catch (error) {}
|
||||
}
|
||||
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
|
||||
};
|
||||
gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
|
||||
var pos;
|
||||
if (!textArea.setSelectionRange) {
|
||||
return;
|
||||
}
|
||||
if (textArea.selectionStart === textArea.selectionEnd) {
|
||||
if (wrapped) {
|
||||
pos = textArea.selectionStart - tag.length;
|
||||
} else {
|
||||
pos = textArea.selectionStart;
|
||||
}
|
||||
if (document.queryCommandSupported('insertText')) {
|
||||
inserted = document.execCommand('insertText', false, insertText);
|
||||
}
|
||||
if (!inserted) {
|
||||
try {
|
||||
document.execCommand("ms-beginUndoUnit");
|
||||
} catch (error) {}
|
||||
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
|
||||
try {
|
||||
document.execCommand("ms-endUndoUnit");
|
||||
} catch (error) {}
|
||||
}
|
||||
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
|
||||
};
|
||||
gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
|
||||
var pos;
|
||||
if (!textArea.setSelectionRange) {
|
||||
return;
|
||||
}
|
||||
if (textArea.selectionStart === textArea.selectionEnd) {
|
||||
if (wrapped) {
|
||||
pos = textArea.selectionStart - tag.length;
|
||||
} else {
|
||||
pos = textArea.selectionStart;
|
||||
}
|
||||
|
||||
if (removedLastNewLine) {
|
||||
pos -= 1;
|
||||
}
|
||||
if (removedLastNewLine) {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
return textArea.setSelectionRange(pos, pos);
|
||||
}
|
||||
};
|
||||
gl.text.updateText = function(textArea, tag, blockTag, wrap) {
|
||||
var $textArea, selected, text;
|
||||
$textArea = $(textArea);
|
||||
textArea = $textArea.get(0);
|
||||
text = $textArea.val();
|
||||
selected = this.selectedText(text, textArea);
|
||||
$textArea.focus();
|
||||
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
|
||||
};
|
||||
gl.text.init = function(form) {
|
||||
var self;
|
||||
self = this;
|
||||
return $('.js-md', form).off('click').on('click', function() {
|
||||
var $this;
|
||||
$this = $(this);
|
||||
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
|
||||
});
|
||||
};
|
||||
gl.text.removeListeners = function(form) {
|
||||
return $('.js-md', form).off();
|
||||
};
|
||||
gl.text.humanize = function(string) {
|
||||
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
|
||||
};
|
||||
gl.text.pluralize = function(str, count) {
|
||||
return str + (count > 1 || count === 0 ? 's' : '');
|
||||
};
|
||||
gl.text.truncate = function(string, maxLength) {
|
||||
return string.substr(0, (maxLength - 3)) + '...';
|
||||
};
|
||||
gl.text.dasherize = function(str) {
|
||||
return str.replace(/[_\s]+/g, '-');
|
||||
};
|
||||
gl.text.slugify = function(str) {
|
||||
return str.trim().toLowerCase().latinise();
|
||||
};
|
||||
})(window);
|
||||
}).call(window);
|
||||
return textArea.setSelectionRange(pos, pos);
|
||||
}
|
||||
};
|
||||
gl.text.updateText = function(textArea, tag, blockTag, wrap) {
|
||||
var $textArea, selected, text;
|
||||
$textArea = $(textArea);
|
||||
textArea = $textArea.get(0);
|
||||
text = $textArea.val();
|
||||
selected = this.selectedText(text, textArea);
|
||||
$textArea.focus();
|
||||
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
|
||||
};
|
||||
gl.text.init = function(form) {
|
||||
var self;
|
||||
self = this;
|
||||
return $('.js-md', form).off('click').on('click', function() {
|
||||
var $this;
|
||||
$this = $(this);
|
||||
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
|
||||
});
|
||||
};
|
||||
gl.text.removeListeners = function(form) {
|
||||
return $('.js-md', form).off();
|
||||
};
|
||||
gl.text.humanize = function(string) {
|
||||
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
|
||||
};
|
||||
gl.text.pluralize = function(str, count) {
|
||||
return str + (count > 1 || count === 0 ? 's' : '');
|
||||
};
|
||||
gl.text.truncate = function(string, maxLength) {
|
||||
return string.substr(0, (maxLength - 3)) + '...';
|
||||
};
|
||||
gl.text.dasherize = function(str) {
|
||||
return str.replace(/[_\s]+/g, '-');
|
||||
};
|
||||
gl.text.slugify = function(str) {
|
||||
return str.trim().toLowerCase().latinise();
|
||||
};
|
||||
|
|
|
@ -1,93 +1,90 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
|
||||
(function() {
|
||||
(function(w) {
|
||||
var base;
|
||||
if (w.gl == null) {
|
||||
w.gl = {};
|
||||
var base;
|
||||
var w = window;
|
||||
if (w.gl == null) {
|
||||
w.gl = {};
|
||||
}
|
||||
if ((base = w.gl).utils == null) {
|
||||
base.utils = {};
|
||||
}
|
||||
// Returns an array containing the value(s) of the
|
||||
// of the key passed as an argument
|
||||
w.gl.utils.getParameterValues = function(sParam) {
|
||||
var i, sPageURL, sParameterName, sURLVariables, values;
|
||||
sPageURL = decodeURIComponent(window.location.search.substring(1));
|
||||
sURLVariables = sPageURL.split('&');
|
||||
sParameterName = void 0;
|
||||
values = [];
|
||||
i = 0;
|
||||
while (i < sURLVariables.length) {
|
||||
sParameterName = sURLVariables[i].split('=');
|
||||
if (sParameterName[0] === sParam) {
|
||||
values.push(sParameterName[1].replace(/\+/g, ' '));
|
||||
}
|
||||
if ((base = w.gl).utils == null) {
|
||||
base.utils = {};
|
||||
i += 1;
|
||||
}
|
||||
return values;
|
||||
};
|
||||
// @param {Object} params - url keys and value to merge
|
||||
// @param {String} url
|
||||
w.gl.utils.mergeUrlParams = function(params, url) {
|
||||
var lastChar, newUrl, paramName, paramValue, pattern;
|
||||
newUrl = decodeURIComponent(url);
|
||||
for (paramName in params) {
|
||||
paramValue = params[paramName];
|
||||
pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
|
||||
if (paramValue == null) {
|
||||
newUrl = newUrl.replace(pattern, '');
|
||||
} else if (url.search(pattern) !== -1) {
|
||||
newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
|
||||
} else {
|
||||
newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
|
||||
}
|
||||
// Returns an array containing the value(s) of the
|
||||
// of the key passed as an argument
|
||||
w.gl.utils.getParameterValues = function(sParam) {
|
||||
var i, sPageURL, sParameterName, sURLVariables, values;
|
||||
sPageURL = decodeURIComponent(window.location.search.substring(1));
|
||||
sURLVariables = sPageURL.split('&');
|
||||
sParameterName = void 0;
|
||||
values = [];
|
||||
i = 0;
|
||||
while (i < sURLVariables.length) {
|
||||
sParameterName = sURLVariables[i].split('=');
|
||||
if (sParameterName[0] === sParam) {
|
||||
values.push(sParameterName[1].replace(/\+/g, ' '));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
// Remove a trailing ampersand
|
||||
lastChar = newUrl[newUrl.length - 1];
|
||||
if (lastChar === '&') {
|
||||
newUrl = newUrl.slice(0, -1);
|
||||
}
|
||||
return newUrl;
|
||||
};
|
||||
// removes parameter query string from url. returns the modified url
|
||||
w.gl.utils.removeParamQueryString = function(url, param) {
|
||||
var urlVariables, variables;
|
||||
url = decodeURIComponent(url);
|
||||
urlVariables = url.split('&');
|
||||
return ((function() {
|
||||
var j, len, results;
|
||||
results = [];
|
||||
for (j = 0, len = urlVariables.length; j < len; j += 1) {
|
||||
variables = urlVariables[j];
|
||||
if (variables.indexOf(param) === -1) {
|
||||
results.push(variables);
|
||||
}
|
||||
return values;
|
||||
};
|
||||
// @param {Object} params - url keys and value to merge
|
||||
// @param {String} url
|
||||
w.gl.utils.mergeUrlParams = function(params, url) {
|
||||
var lastChar, newUrl, paramName, paramValue, pattern;
|
||||
newUrl = decodeURIComponent(url);
|
||||
for (paramName in params) {
|
||||
paramValue = params[paramName];
|
||||
pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
|
||||
if (paramValue == null) {
|
||||
newUrl = newUrl.replace(pattern, '');
|
||||
} else if (url.search(pattern) !== -1) {
|
||||
newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
|
||||
} else {
|
||||
newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
|
||||
}
|
||||
}
|
||||
// Remove a trailing ampersand
|
||||
lastChar = newUrl[newUrl.length - 1];
|
||||
if (lastChar === '&') {
|
||||
newUrl = newUrl.slice(0, -1);
|
||||
}
|
||||
return newUrl;
|
||||
};
|
||||
// removes parameter query string from url. returns the modified url
|
||||
w.gl.utils.removeParamQueryString = function(url, param) {
|
||||
var urlVariables, variables;
|
||||
url = decodeURIComponent(url);
|
||||
urlVariables = url.split('&');
|
||||
return ((function() {
|
||||
var j, len, results;
|
||||
results = [];
|
||||
for (j = 0, len = urlVariables.length; j < len; j += 1) {
|
||||
variables = urlVariables[j];
|
||||
if (variables.indexOf(param) === -1) {
|
||||
results.push(variables);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
})()).join('&');
|
||||
};
|
||||
w.gl.utils.removeParams = (params) => {
|
||||
const url = new URL(window.location.href);
|
||||
params.forEach((param) => {
|
||||
url.search = w.gl.utils.removeParamQueryString(url.search, param);
|
||||
});
|
||||
return url.href;
|
||||
};
|
||||
w.gl.utils.getLocationHash = function(url) {
|
||||
var hashIndex;
|
||||
if (typeof url === 'undefined') {
|
||||
// Note: We can't use window.location.hash here because it's
|
||||
// not consistent across browsers - Firefox will pre-decode it
|
||||
url = window.location.href;
|
||||
}
|
||||
hashIndex = url.indexOf('#');
|
||||
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
|
||||
};
|
||||
}
|
||||
return results;
|
||||
})()).join('&');
|
||||
};
|
||||
w.gl.utils.removeParams = (params) => {
|
||||
const url = new URL(window.location.href);
|
||||
params.forEach((param) => {
|
||||
url.search = w.gl.utils.removeParamQueryString(url.search, param);
|
||||
});
|
||||
return url.href;
|
||||
};
|
||||
w.gl.utils.getLocationHash = function(url) {
|
||||
var hashIndex;
|
||||
if (typeof url === 'undefined') {
|
||||
// Note: We can't use window.location.hash here because it's
|
||||
// not consistent across browsers - Firefox will pre-decode it
|
||||
url = window.location.href;
|
||||
}
|
||||
hashIndex = url.indexOf('#');
|
||||
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
|
||||
};
|
||||
|
||||
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
|
||||
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
|
||||
|
||||
w.gl.utils.visitUrl = (url) => {
|
||||
document.location.href = url;
|
||||
};
|
||||
})(window);
|
||||
}).call(window);
|
||||
w.gl.utils.visitUrl = (url) => {
|
||||
document.location.href = url;
|
||||
};
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len */
|
||||
|
||||
(function() {
|
||||
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
|
||||
import '~/lib/utils/url_utility';
|
||||
|
||||
(function() {
|
||||
this.MergedButtons = (function() {
|
||||
function MergedButtons() {
|
||||
this.removeSourceBranch = bind(this.removeSourceBranch, this);
|
||||
this.removeSourceBranch = this.removeSourceBranch.bind(this);
|
||||
this.removeBranchSuccess = this.removeBranchSuccess.bind(this);
|
||||
this.removeBranchError = this.removeBranchError.bind(this);
|
||||
this.$removeBranchWidget = $('.remove_source_branch_widget');
|
||||
this.$removeBranchProgress = $('.remove_source_branch_in_progress');
|
||||
this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
|
||||
|
@ -22,7 +24,7 @@
|
|||
MergedButtons.prototype.initEventListeners = function() {
|
||||
$(document).on('click', '.remove_source_branch', this.removeSourceBranch);
|
||||
$(document).on('ajax:success', '.remove_source_branch', this.removeBranchSuccess);
|
||||
return $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError);
|
||||
$(document).on('ajax:error', '.remove_source_branch', this.removeBranchError);
|
||||
};
|
||||
|
||||
MergedButtons.prototype.removeSourceBranch = function() {
|
||||
|
@ -31,7 +33,7 @@
|
|||
};
|
||||
|
||||
MergedButtons.prototype.removeBranchSuccess = function() {
|
||||
return location.reload();
|
||||
gl.utils.refreshCurrentPage();
|
||||
};
|
||||
|
||||
MergedButtons.prototype.removeBranchError = function() {
|
||||
|
|
|
@ -246,17 +246,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.filtered-search-history-dropdown-toggle-button {
|
||||
.filtered-search-history-dropdown-wrapper {
|
||||
position: static;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
padding-top: 0;
|
||||
padding-left: 0.75em;
|
||||
padding-bottom: 0;
|
||||
padding-right: 0.5em;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filtered-search-history-dropdown-toggle-button {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
padding-right: 10px;
|
||||
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
|
@ -264,6 +264,7 @@
|
|||
border-right: 1px solid $border-color;
|
||||
|
||||
color: $gl-text-color-secondary;
|
||||
line-height: 1;
|
||||
|
||||
transition: color 0.1s linear;
|
||||
|
||||
|
@ -275,24 +276,21 @@
|
|||
}
|
||||
|
||||
.dropdown-toggle-text {
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
|
||||
.fa {
|
||||
vertical-align: middle;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.fa {
|
||||
position: initial;
|
||||
position: static;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.filtered-search-history-dropdown-wrapper {
|
||||
position: initial;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filtered-search-history-dropdown {
|
||||
width: 40%;
|
||||
|
||||
|
|
|
@ -158,6 +158,7 @@
|
|||
li.task-list-item {
|
||||
list-style-type: none;
|
||||
position: relative;
|
||||
min-height: 22px;
|
||||
padding-left: 28px;
|
||||
margin-left: 0 !important;
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
|
|||
$gray-darker: #eee;
|
||||
$gray-darkest: #c4c4c4;
|
||||
|
||||
$green-25: #f6fcf8;
|
||||
$green-50: #e4f5eb;
|
||||
$green-100: #bae6cc;
|
||||
$green-200: #8dd5aa;
|
||||
|
@ -37,6 +38,7 @@ $green-700: #12753a;
|
|||
$green-800: #0e5a2d;
|
||||
$green-900: #0a4020;
|
||||
|
||||
$blue-25: #f6fafd;
|
||||
$blue-50: #e4eff9;
|
||||
$blue-100: #bcd7f1;
|
||||
$blue-200: #8fbce8;
|
||||
|
@ -48,6 +50,7 @@ $blue-700: #17599c;
|
|||
$blue-800: #134a81;
|
||||
$blue-900: #0f3b66;
|
||||
|
||||
$orange-25: #fffcf8;
|
||||
$orange-50: #fff2e1;
|
||||
$orange-100: #fedfb3;
|
||||
$orange-200: #feca81;
|
||||
|
@ -59,6 +62,7 @@ $orange-700: #c26700;
|
|||
$orange-800: #a35100;
|
||||
$orange-900: #853b00;
|
||||
|
||||
$red-25: #fef7f6;
|
||||
$red-50: #fbe7e4;
|
||||
$red-100: #f4c4bc;
|
||||
$red-200: #ed9d90;
|
||||
|
@ -147,7 +151,7 @@ $gl-sidebar-padding: 22px;
|
|||
/*
|
||||
* Misc
|
||||
*/
|
||||
$row-hover: lighten($blue-50, 2%);
|
||||
$row-hover: $blue-25;
|
||||
$row-hover-border: $blue-100;
|
||||
$progress-color: #c0392b;
|
||||
$header-height: 50px;
|
||||
|
@ -223,18 +227,18 @@ $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background;
|
|||
/*
|
||||
* Commit Diff Colors
|
||||
*/
|
||||
$added: $green-300;
|
||||
$deleted: $red-300;
|
||||
$line-added: $green-50;
|
||||
$line-added-dark: $green-100;
|
||||
$line-removed: $red-50;
|
||||
$line-removed-dark: $red-100;
|
||||
$line-number-old: lighten($red-100, 5%);
|
||||
$line-number-new: lighten($green-100, 5%);
|
||||
$line-number-select: lighten($orange-100, 5%);
|
||||
$line-target-blue: $blue-50;
|
||||
$line-select-yellow: $orange-50;
|
||||
$line-select-yellow-dark: $orange-100;
|
||||
$added: #63c363;
|
||||
$deleted: #f77;
|
||||
$line-added: #ecfdf0;
|
||||
$line-added-dark: #c7f0d2;
|
||||
$line-removed: #fbe9eb;
|
||||
$line-removed-dark: #fac5cd;
|
||||
$line-number-old: #f9d7dc;
|
||||
$line-number-new: #ddfbe6;
|
||||
$line-number-select: #fbf2da;
|
||||
$line-target-blue: #f6faff;
|
||||
$line-select-yellow: #fcf8e7;
|
||||
$line-select-yellow-dark: #f0e2bd;
|
||||
$dark-diff-match-bg: rgba(255, 255, 255, 0.3);
|
||||
$dark-diff-match-color: rgba(255, 255, 255, 0.1);
|
||||
$file-mode-changed: #777;
|
||||
|
|
|
@ -627,7 +627,6 @@ ul.notes {
|
|||
}
|
||||
|
||||
&:not(.is-disabled):hover,
|
||||
&:not(.is-disabled):focus,
|
||||
&.is-active {
|
||||
color: $gl-text-green;
|
||||
|
||||
|
@ -641,6 +640,11 @@ ul.notes {
|
|||
height: 15px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin: 0;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-next-btn {
|
||||
|
|
|
@ -60,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
|
||||
def resource
|
||||
@resource ||= Users::CreateService.new(current_user, sign_up_params).build
|
||||
@resource ||= Users::BuildService.new(current_user, sign_up_params).execute
|
||||
end
|
||||
|
||||
def devise_mapping
|
||||
|
|
|
@ -20,7 +20,8 @@ class ContainerRepository < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def path
|
||||
@path ||= [project.full_path, name].select(&:present?).join('/')
|
||||
@path ||= [project.full_path, name]
|
||||
.select(&:present?).join('/').downcase
|
||||
end
|
||||
|
||||
def location
|
||||
|
|
|
@ -58,6 +58,9 @@ module Projects
|
|||
fail(error: @project.errors.full_messages.join(', '))
|
||||
end
|
||||
@project
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} "
|
||||
fail(error: message)
|
||||
rescue => e
|
||||
fail(error: e.message)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
module Users
|
||||
# Service for building a new user.
|
||||
class BuildService < BaseService
|
||||
def initialize(current_user, params = {})
|
||||
@current_user = current_user
|
||||
@params = params.dup
|
||||
end
|
||||
|
||||
def execute
|
||||
raise Gitlab::Access::AccessDeniedError unless can_create_user?
|
||||
|
||||
user = User.new(build_user_params)
|
||||
|
||||
if current_user&.admin?
|
||||
if params[:reset_password]
|
||||
user.generate_reset_token
|
||||
params[:force_random_password] = true
|
||||
end
|
||||
|
||||
if params[:force_random_password]
|
||||
random_password = Devise.friendly_token.first(Devise.password_length.min)
|
||||
user.password = user.password_confirmation = random_password
|
||||
end
|
||||
end
|
||||
|
||||
identity_attrs = params.slice(:extern_uid, :provider)
|
||||
|
||||
if identity_attrs.any?
|
||||
user.identities.build(identity_attrs)
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_create_user?
|
||||
(current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin?
|
||||
end
|
||||
|
||||
# Allowed params for creating a user (admins only)
|
||||
def admin_create_params
|
||||
[
|
||||
:access_level,
|
||||
:admin,
|
||||
:avatar,
|
||||
:bio,
|
||||
:can_create_group,
|
||||
:color_scheme_id,
|
||||
:email,
|
||||
:external,
|
||||
:force_random_password,
|
||||
:hide_no_password,
|
||||
:hide_no_ssh_key,
|
||||
:key_id,
|
||||
:linkedin,
|
||||
:name,
|
||||
:password,
|
||||
:password_automatically_set,
|
||||
:password_expires_at,
|
||||
:projects_limit,
|
||||
:remember_me,
|
||||
:skip_confirmation,
|
||||
:skype,
|
||||
:theme_id,
|
||||
:twitter,
|
||||
:username,
|
||||
:website_url
|
||||
]
|
||||
end
|
||||
|
||||
# Allowed params for user signup
|
||||
def signup_params
|
||||
[
|
||||
:email,
|
||||
:email_confirmation,
|
||||
:password_automatically_set,
|
||||
:name,
|
||||
:password,
|
||||
:username
|
||||
]
|
||||
end
|
||||
|
||||
def build_user_params
|
||||
if current_user&.admin?
|
||||
user_params = params.slice(*admin_create_params)
|
||||
user_params[:created_by_id] = current_user&.id
|
||||
|
||||
if params[:reset_password]
|
||||
user_params.merge!(force_random_password: true, password_expires_at: nil)
|
||||
end
|
||||
else
|
||||
user_params = params.slice(*signup_params)
|
||||
user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
|
||||
end
|
||||
|
||||
user_params
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,34 +6,10 @@ module Users
|
|||
@params = params.dup
|
||||
end
|
||||
|
||||
def build
|
||||
raise Gitlab::Access::AccessDeniedError unless can_create_user?
|
||||
|
||||
user = User.new(build_user_params)
|
||||
|
||||
if current_user&.admin?
|
||||
if params[:reset_password]
|
||||
@reset_token = user.generate_reset_token
|
||||
params[:force_random_password] = true
|
||||
end
|
||||
|
||||
if params[:force_random_password]
|
||||
random_password = Devise.friendly_token.first(Devise.password_length.min)
|
||||
user.password = user.password_confirmation = random_password
|
||||
end
|
||||
end
|
||||
|
||||
identity_attrs = params.slice(:extern_uid, :provider)
|
||||
|
||||
if identity_attrs.any?
|
||||
user.identities.build(identity_attrs)
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def execute
|
||||
user = build
|
||||
user = Users::BuildService.new(current_user, params).execute
|
||||
|
||||
@reset_token = user.generate_reset_token if user.recently_sent_password_reset?
|
||||
|
||||
if user.save
|
||||
log_info("User \"#{user.name}\" (#{user.email}) was created")
|
||||
|
@ -43,70 +19,5 @@ module Users
|
|||
|
||||
user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_create_user?
|
||||
(current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin?
|
||||
end
|
||||
|
||||
# Allowed params for creating a user (admins only)
|
||||
def admin_create_params
|
||||
[
|
||||
:access_level,
|
||||
:admin,
|
||||
:avatar,
|
||||
:bio,
|
||||
:can_create_group,
|
||||
:color_scheme_id,
|
||||
:email,
|
||||
:external,
|
||||
:force_random_password,
|
||||
:password_automatically_set,
|
||||
:hide_no_password,
|
||||
:hide_no_ssh_key,
|
||||
:key_id,
|
||||
:linkedin,
|
||||
:name,
|
||||
:password,
|
||||
:password_expires_at,
|
||||
:projects_limit,
|
||||
:remember_me,
|
||||
:skip_confirmation,
|
||||
:skype,
|
||||
:theme_id,
|
||||
:twitter,
|
||||
:username,
|
||||
:website_url
|
||||
]
|
||||
end
|
||||
|
||||
# Allowed params for user signup
|
||||
def signup_params
|
||||
[
|
||||
:email,
|
||||
:email_confirmation,
|
||||
:password_automatically_set,
|
||||
:name,
|
||||
:password,
|
||||
:username
|
||||
]
|
||||
end
|
||||
|
||||
def build_user_params
|
||||
if current_user&.admin?
|
||||
user_params = params.slice(*admin_create_params)
|
||||
user_params[:created_by_id] = current_user&.id
|
||||
|
||||
if params[:reset_password]
|
||||
user_params.merge!(force_random_password: true, password_expires_at: nil)
|
||||
end
|
||||
else
|
||||
user_params = params.slice(*signup_params)
|
||||
user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
|
||||
end
|
||||
|
||||
user_params
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
- @services.sort_by(&:title).each do |service|
|
||||
%tr
|
||||
%td
|
||||
= icon("copy", class: 'clgray')
|
||||
= boolean_to_icon service.activated?
|
||||
%td
|
||||
= link_to edit_admin_application_settings_service_path(service.id) do
|
||||
%strong= service.title
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
Registry
|
||||
|
||||
- if project_nav_tab? :issues
|
||||
= nav_link(controller: [:issues, :labels, :milestones, :boards]) do
|
||||
= nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do
|
||||
= link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do
|
||||
%span
|
||||
Issues
|
||||
|
@ -31,7 +31,7 @@
|
|||
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
|
||||
|
||||
- if project_nav_tab? :merge_requests
|
||||
= nav_link(controller: :merge_requests) do
|
||||
= nav_link(controller: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
|
||||
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
|
||||
%span
|
||||
Merge Requests
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- @no_container = true
|
||||
- page_title "Edit", @label.name, "Labels"
|
||||
= render "projects/issues/head"
|
||||
= render "shared/mr_head"
|
||||
|
||||
%div{ class: container_class }
|
||||
%h3.page-title
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- @no_container = true
|
||||
- page_title "Labels"
|
||||
- hide_class = ''
|
||||
= render "projects/issues/head"
|
||||
= render "shared/mr_head"
|
||||
|
||||
- if @labels.exists? || @prioritized_labels.exists?
|
||||
%div{ class: container_class }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- @no_container = true
|
||||
- page_title "New Label"
|
||||
= render "projects/issues/head"
|
||||
= render "shared/mr_head"
|
||||
|
||||
%div{ class: container_class }
|
||||
%h3.page-title
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
= content_for :sub_nav do
|
||||
.scrolling-tabs-container.sub-nav-scroll
|
||||
= render 'shared/nav_scroll'
|
||||
.nav-links.sub-nav.scrolling-tabs
|
||||
%ul{ class: (container_class) }
|
||||
= nav_link(controller: :merge_requests) do
|
||||
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
|
||||
%span
|
||||
List
|
||||
|
||||
- if project_nav_tab? :labels
|
||||
= nav_link(controller: :labels) do
|
||||
= link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
|
||||
%span
|
||||
Labels
|
||||
|
||||
- if project_nav_tab? :milestones
|
||||
= nav_link(controller: :milestones) do
|
||||
= link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
|
||||
%span
|
||||
Milestones
|
|
@ -2,6 +2,9 @@
|
|||
- @bulk_edit = can?(current_user, :admin_merge_request, @project)
|
||||
|
||||
- page_title "Merge Requests"
|
||||
- unless @project.default_issues_tracker?
|
||||
= content_for :sub_nav do
|
||||
= render "projects/merge_requests/head"
|
||||
= render 'projects/last_push'
|
||||
|
||||
- content_for :page_specific_javascripts do
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- @no_container = true
|
||||
- page_title "Edit", @milestone.title, "Milestones"
|
||||
= render "projects/issues/head"
|
||||
= render "shared/mr_head"
|
||||
|
||||
%div{ class: container_class }
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- @no_container = true
|
||||
- page_title 'Milestones'
|
||||
= render 'projects/issues/head'
|
||||
= render "shared/mr_head"
|
||||
|
||||
%div{ class: container_class }
|
||||
.top-area
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- @no_container = true
|
||||
- page_title "New Milestone"
|
||||
= render "projects/issues/head"
|
||||
= render "shared/mr_head"
|
||||
|
||||
%div{ class: container_class }
|
||||
%h3.page-title
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- @no_container = true
|
||||
- page_title @milestone.title, "Milestones"
|
||||
- page_description @milestone.description
|
||||
= render "projects/issues/head"
|
||||
= render "shared/mr_head"
|
||||
|
||||
%div{ class: container_class }
|
||||
.detail-page-header.milestone-page-header
|
||||
|
|
|
@ -52,11 +52,10 @@
|
|||
":aria-label" => "buttonText",
|
||||
"@click" => "resolve",
|
||||
":title" => "buttonText",
|
||||
"v-show" => "!loading",
|
||||
":ref" => "'button'" }
|
||||
= icon("spin spinner", "v-show" => "loading")
|
||||
|
||||
= render "shared/icons/icon_status_success.svg"
|
||||
= icon("spin spinner", "v-show" => "loading", class: 'loading')
|
||||
%div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg"
|
||||
|
||||
- if current_user
|
||||
- if note.emoji_awardable?
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
= render "projects/last_push"
|
||||
= render "home_panel"
|
||||
|
||||
- if current_user && can?(current_user, :download_code, @project)
|
||||
- if can?(current_user, :download_code, @project)
|
||||
%nav.project-stats{ class: container_class }
|
||||
%ul.nav
|
||||
%li
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
= f.label :import_url, class: 'control-label' do
|
||||
%span Git repository URL
|
||||
.col-sm-10
|
||||
= f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true
|
||||
= f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
|
||||
|
||||
.well.prepend-top-20
|
||||
%ul
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
- if @project.default_issues_tracker?
|
||||
= render "projects/issues/head"
|
||||
- else
|
||||
= render "projects/merge_requests/head"
|
|
@ -16,6 +16,8 @@
|
|||
Also, issues are searchable and filterable.
|
||||
- if project_select_button
|
||||
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
|
||||
= link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
|
||||
- else
|
||||
%h4 There are no issues to show.
|
||||
= link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
|
||||
.text-center
|
||||
%h4 There are no issues to show.
|
||||
= link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Implement Users::BuildService
|
||||
merge_request: 30349
|
||||
author: George Andrinopoulos
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Show sub-nav under Merge Requests when issue tracker is non-default.
|
||||
merge_request: 10658
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fixed alignment of empty task list items
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix invalid encoding when showing some traces
|
||||
merge_request: 10681
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Centered issues empty state
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add lighter colors and fix existing light colors
|
||||
merge_request: 10690
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add hashie-forbidden_attributes gem
|
||||
merge_request: 10579
|
||||
author: Andy Brown
|
|
@ -50,20 +50,17 @@ update them are in [a separate document][omnidocker].
|
|||
|
||||
## Upgrading without downtime
|
||||
|
||||
Starting with GitLab 9.1.0 it's possible to upgrade to a newer version of GitLab
|
||||
Starting with GitLab 9.1.0 it's possible to upgrade to a newer major, minor, or patch version of GitLab
|
||||
without having to take your GitLab instance offline. However, for this to work
|
||||
there are the following requirements:
|
||||
|
||||
1. You can only upgrade 1 release at a time. For example, if 9.1.15 is the last
|
||||
release of 9.1 then you can safely upgrade from that version to 9.2.0.
|
||||
1. You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to 9.3.
|
||||
2. You have to be on the most recent patch release. For example, if 9.1.15 is the last
|
||||
release of 9.1 then you can safely upgrade from that version to any 9.2.x version.
|
||||
However, if you are running 9.1.14 you first need to upgrade to 9.1.15.
|
||||
2. You have to use [post-deployment
|
||||
migrations](../development/post_deployment_migrations.md).
|
||||
3. You are using PostgreSQL. If you are using MySQL you will still need downtime
|
||||
when upgrading.
|
||||
|
||||
This applies to major, minor, and patch releases unless stated otherwise in a
|
||||
release post.
|
||||
3. You are using PostgreSQL. If you are using MySQL please look at the release post to see if downtime is required.
|
||||
|
||||
## Upgrading between editions
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@ module Banzai
|
|||
issuables = extractor.extract([doc])
|
||||
|
||||
issuables.each do |node, issuable|
|
||||
if VISIBLE_STATES.include?(issuable.state)
|
||||
node.children.last.content += " [#{issuable.state}]"
|
||||
if VISIBLE_STATES.include?(issuable.state) && node.children.present?
|
||||
node.add_child(Nokogiri::XML::Text.new(" [#{issuable.state}]", doc))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ module ContainerRegistry
|
|||
LEVELS_SUPPORTED = 3
|
||||
|
||||
def initialize(path)
|
||||
@path = path
|
||||
@path = path.to_s.downcase
|
||||
end
|
||||
|
||||
def valid?
|
||||
|
@ -25,7 +25,7 @@ module ContainerRegistry
|
|||
end
|
||||
|
||||
def components
|
||||
@components ||= @path.to_s.split('/')
|
||||
@components ||= @path.split('/')
|
||||
end
|
||||
|
||||
def nodes
|
||||
|
|
|
@ -25,11 +25,10 @@ module Gitlab
|
|||
end
|
||||
|
||||
def limit(last_bytes = LIMIT_SIZE)
|
||||
stream_size = size
|
||||
if stream_size < last_bytes
|
||||
last_bytes = stream_size
|
||||
if last_bytes < size
|
||||
stream.seek(-last_bytes, IO::SEEK_END)
|
||||
stream.readline
|
||||
end
|
||||
stream.seek(-last_bytes, IO::SEEK_END)
|
||||
end
|
||||
|
||||
def append(data, offset)
|
||||
|
|
|
@ -148,7 +148,7 @@ module Gitlab
|
|||
|
||||
def build_new_user
|
||||
user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true)
|
||||
Users::CreateService.new(nil, user_params).build
|
||||
Users::BuildService.new(nil, user_params).execute
|
||||
end
|
||||
|
||||
def user_attributes
|
||||
|
|
|
@ -7,10 +7,10 @@ namespace :gitlab do
|
|||
abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]")
|
||||
end
|
||||
|
||||
tag = "v#{Gitlab::GitalyClient.expected_server_version}"
|
||||
version = Gitlab::GitalyClient.expected_server_version
|
||||
repo = 'https://gitlab.com/gitlab-org/gitaly.git'
|
||||
|
||||
checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir)
|
||||
checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir)
|
||||
|
||||
_, status = Gitlab::Popen.popen(%w[which gmake])
|
||||
command = status.zero? ? 'gmake' : 'make'
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
namespace :gitlab do
|
||||
namespace :shell do
|
||||
desc "GitLab | Install or upgrade gitlab-shell"
|
||||
task :install, [:tag, :repo] => :environment do |t, args|
|
||||
task :install, [:repo] => :environment do |t, args|
|
||||
warn_user_is_not_gitlab
|
||||
|
||||
default_version = Gitlab::Shell.version_required
|
||||
default_version_tag = "v#{default_version}"
|
||||
args.with_defaults(tag: default_version_tag, repo: 'https://gitlab.com/gitlab-org/gitlab-shell.git')
|
||||
args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitlab-shell.git')
|
||||
|
||||
gitlab_url = Gitlab.config.gitlab.url
|
||||
# gitlab-shell requires a / at the end of the url
|
||||
gitlab_url += '/' unless gitlab_url.end_with?('/')
|
||||
target_dir = Gitlab.config.gitlab_shell.path
|
||||
|
||||
checkout_or_clone_tag(tag: default_version_tag, repo: args.repo, target_dir: target_dir)
|
||||
checkout_or_clone_version(version: default_version, repo: args.repo, target_dir: target_dir)
|
||||
|
||||
# Make sure we're on the right tag
|
||||
Dir.chdir(target_dir) do
|
||||
|
|
|
@ -147,41 +147,30 @@ module Gitlab
|
|||
Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home
|
||||
end
|
||||
|
||||
def checkout_or_clone_tag(tag:, repo:, target_dir:)
|
||||
if Dir.exist?(target_dir)
|
||||
checkout_tag(tag, target_dir)
|
||||
else
|
||||
clone_repo(repo, target_dir)
|
||||
end
|
||||
def checkout_or_clone_version(version:, repo:, target_dir:)
|
||||
version =
|
||||
if version.starts_with?("=")
|
||||
version.sub(/\A=/, '') # tag or branch
|
||||
else
|
||||
"v#{version}" # tag
|
||||
end
|
||||
|
||||
reset_to_tag(tag, target_dir)
|
||||
clone_repo(repo, target_dir) unless Dir.exist?(target_dir)
|
||||
checkout_version(version, target_dir)
|
||||
reset_to_version(version, target_dir)
|
||||
end
|
||||
|
||||
def clone_repo(repo, target_dir)
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}])
|
||||
end
|
||||
|
||||
def checkout_tag(tag, target_dir)
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --tags --quiet])
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout --quiet #{tag}])
|
||||
def checkout_version(version, target_dir)
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --quiet])
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout --quiet #{version}])
|
||||
end
|
||||
|
||||
def reset_to_tag(tag_wanted, target_dir)
|
||||
tag =
|
||||
begin
|
||||
# First try to checkout without fetching
|
||||
# to avoid stalling tests if the Internet is down.
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
|
||||
rescue Gitlab::TaskFailedError
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
|
||||
end
|
||||
|
||||
if tag
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{tag.strip}])
|
||||
else
|
||||
raise Gitlab::TaskFailedError
|
||||
end
|
||||
def reset_to_version(version, target_dir)
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{version}])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,10 +7,10 @@ namespace :gitlab do
|
|||
abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]")
|
||||
end
|
||||
|
||||
tag = "v#{Gitlab::Workhorse.version}"
|
||||
version = Gitlab::Workhorse.version
|
||||
repo = 'https://gitlab.com/gitlab-org/gitlab-workhorse.git'
|
||||
|
||||
checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir)
|
||||
checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir)
|
||||
|
||||
_, status = Gitlab::Popen.popen(%w[which gmake])
|
||||
command = status.zero? ? 'gmake' : 'make'
|
||||
|
|
|
@ -198,6 +198,8 @@ feature 'Diff notes resolve', feature: true, js: true do
|
|||
it 'does not mark discussion as resolved when resolving single note' do
|
||||
page.first '.diff-content .note' do
|
||||
first('.line-resolve-btn').click
|
||||
|
||||
expect(page).to have_selector('.note-action-button .loading')
|
||||
expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
[0m[01;34m.[0m
|
||||
[30;42m..[0m
|
||||
😺
|
||||
ヾ(´༎ຶД༎ຶ`)ノ
|
||||
[01;32m許功蓋[0m
|
|
@ -7,6 +7,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
|
|||
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
|
||||
let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') }
|
||||
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') }
|
||||
let(:merged_merge_request) { create(:merge_request, :merged, source_project: project, target_project: project) }
|
||||
let(:pipeline) do
|
||||
create(
|
||||
:ci_pipeline,
|
||||
|
@ -32,6 +33,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
|
|||
render_merge_request(example.description, merge_request)
|
||||
end
|
||||
|
||||
it 'merge_requests/merged_merge_request.html.raw' do |example|
|
||||
allow_any_instance_of(MergeRequest).to receive(:source_branch_exists?).and_return(true)
|
||||
allow_any_instance_of(MergeRequest).to receive(:can_remove_source_branch?).and_return(true)
|
||||
render_merge_request(example.description, merged_merge_request)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_merge_request(fixture_file_name, merge_request)
|
||||
|
|
|
@ -1,110 +1,108 @@
|
|||
require('~/lib/utils/text_utility');
|
||||
|
||||
(() => {
|
||||
describe('text_utility', () => {
|
||||
describe('gl.text.getTextWidth', () => {
|
||||
it('returns zero width when no text is passed', () => {
|
||||
expect(gl.text.getTextWidth('')).toBe(0);
|
||||
});
|
||||
|
||||
it('returns zero width when no text is passed and font is passed', () => {
|
||||
expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
|
||||
});
|
||||
|
||||
it('returns width when text is passed', () => {
|
||||
expect(gl.text.getTextWidth('foo') > 0).toBe(true);
|
||||
});
|
||||
|
||||
it('returns bigger width when font is larger', () => {
|
||||
const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
|
||||
const regular = gl.text.getTextWidth('foo', '10px sans-serif');
|
||||
expect(largeFont > regular).toBe(true);
|
||||
});
|
||||
describe('text_utility', () => {
|
||||
describe('gl.text.getTextWidth', () => {
|
||||
it('returns zero width when no text is passed', () => {
|
||||
expect(gl.text.getTextWidth('')).toBe(0);
|
||||
});
|
||||
|
||||
describe('gl.text.pluralize', () => {
|
||||
it('returns pluralized', () => {
|
||||
expect(gl.text.pluralize('test', 2)).toBe('tests');
|
||||
});
|
||||
|
||||
it('returns pluralized when count is 0', () => {
|
||||
expect(gl.text.pluralize('test', 0)).toBe('tests');
|
||||
});
|
||||
|
||||
it('does not return pluralized', () => {
|
||||
expect(gl.text.pluralize('test', 1)).toBe('test');
|
||||
});
|
||||
it('returns zero width when no text is passed and font is passed', () => {
|
||||
expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
|
||||
});
|
||||
|
||||
describe('gl.text.highCountTrim', () => {
|
||||
it('returns 99+ for count >= 100', () => {
|
||||
expect(gl.text.highCountTrim(105)).toBe('99+');
|
||||
expect(gl.text.highCountTrim(100)).toBe('99+');
|
||||
});
|
||||
|
||||
it('returns exact number for count < 100', () => {
|
||||
expect(gl.text.highCountTrim(45)).toBe(45);
|
||||
});
|
||||
it('returns width when text is passed', () => {
|
||||
expect(gl.text.getTextWidth('foo') > 0).toBe(true);
|
||||
});
|
||||
|
||||
describe('gl.text.insertText', () => {
|
||||
let textArea;
|
||||
it('returns bigger width when font is larger', () => {
|
||||
const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
|
||||
const regular = gl.text.getTextWidth('foo', '10px sans-serif');
|
||||
expect(largeFont > regular).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
textArea = document.createElement('textarea');
|
||||
document.querySelector('body').appendChild(textArea);
|
||||
describe('gl.text.pluralize', () => {
|
||||
it('returns pluralized', () => {
|
||||
expect(gl.text.pluralize('test', 2)).toBe('tests');
|
||||
});
|
||||
|
||||
it('returns pluralized when count is 0', () => {
|
||||
expect(gl.text.pluralize('test', 0)).toBe('tests');
|
||||
});
|
||||
|
||||
it('does not return pluralized', () => {
|
||||
expect(gl.text.pluralize('test', 1)).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gl.text.highCountTrim', () => {
|
||||
it('returns 99+ for count >= 100', () => {
|
||||
expect(gl.text.highCountTrim(105)).toBe('99+');
|
||||
expect(gl.text.highCountTrim(100)).toBe('99+');
|
||||
});
|
||||
|
||||
it('returns exact number for count < 100', () => {
|
||||
expect(gl.text.highCountTrim(45)).toBe(45);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gl.text.insertText', () => {
|
||||
let textArea;
|
||||
|
||||
beforeAll(() => {
|
||||
textArea = document.createElement('textarea');
|
||||
document.querySelector('body').appendChild(textArea);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
textArea.parentNode.removeChild(textArea);
|
||||
});
|
||||
|
||||
describe('without selection', () => {
|
||||
it('inserts the tag on an empty line', () => {
|
||||
const initialValue = '';
|
||||
|
||||
textArea.value = initialValue;
|
||||
textArea.selectionStart = 0;
|
||||
textArea.selectionEnd = 0;
|
||||
|
||||
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
|
||||
|
||||
expect(textArea.value).toEqual(`${initialValue}* `);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
textArea.parentNode.removeChild(textArea);
|
||||
it('inserts the tag on a new line if the current one is not empty', () => {
|
||||
const initialValue = 'some text';
|
||||
|
||||
textArea.value = initialValue;
|
||||
textArea.setSelectionRange(initialValue.length, initialValue.length);
|
||||
|
||||
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
|
||||
|
||||
expect(textArea.value).toEqual(`${initialValue}\n* `);
|
||||
});
|
||||
|
||||
describe('without selection', () => {
|
||||
it('inserts the tag on an empty line', () => {
|
||||
const initialValue = '';
|
||||
it('inserts the tag on the same line if the current line only contains spaces', () => {
|
||||
const initialValue = ' ';
|
||||
|
||||
textArea.value = initialValue;
|
||||
textArea.selectionStart = 0;
|
||||
textArea.selectionEnd = 0;
|
||||
textArea.value = initialValue;
|
||||
textArea.setSelectionRange(initialValue.length, initialValue.length);
|
||||
|
||||
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
|
||||
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
|
||||
|
||||
expect(textArea.value).toEqual(`${initialValue}* `);
|
||||
});
|
||||
expect(textArea.value).toEqual(`${initialValue}* `);
|
||||
});
|
||||
|
||||
it('inserts the tag on a new line if the current one is not empty', () => {
|
||||
const initialValue = 'some text';
|
||||
it('inserts the tag on the same line if the current line only contains tabs', () => {
|
||||
const initialValue = '\t\t\t';
|
||||
|
||||
textArea.value = initialValue;
|
||||
textArea.setSelectionRange(initialValue.length, initialValue.length);
|
||||
textArea.value = initialValue;
|
||||
textArea.setSelectionRange(initialValue.length, initialValue.length);
|
||||
|
||||
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
|
||||
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
|
||||
|
||||
expect(textArea.value).toEqual(`${initialValue}\n* `);
|
||||
});
|
||||
|
||||
it('inserts the tag on the same line if the current line only contains spaces', () => {
|
||||
const initialValue = ' ';
|
||||
|
||||
textArea.value = initialValue;
|
||||
textArea.setSelectionRange(initialValue.length, initialValue.length);
|
||||
|
||||
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
|
||||
|
||||
expect(textArea.value).toEqual(`${initialValue}* `);
|
||||
});
|
||||
|
||||
it('inserts the tag on the same line if the current line only contains tabs', () => {
|
||||
const initialValue = '\t\t\t';
|
||||
|
||||
textArea.value = initialValue;
|
||||
textArea.setSelectionRange(initialValue.length, initialValue.length);
|
||||
|
||||
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
|
||||
|
||||
expect(textArea.value).toEqual(`${initialValue}* `);
|
||||
});
|
||||
expect(textArea.value).toEqual(`${initialValue}* `);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/* global MergedButtons */
|
||||
|
||||
import '~/merged_buttons';
|
||||
|
||||
describe('MergedButtons', () => {
|
||||
const fixturesPath = 'merge_requests/merged_merge_request.html.raw';
|
||||
preloadFixtures(fixturesPath);
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures(fixturesPath);
|
||||
this.mergedButtons = new MergedButtons();
|
||||
this.$removeBranchWidget = $('.remove_source_branch_widget:not(.failed)');
|
||||
this.$removeBranchProgress = $('.remove_source_branch_in_progress');
|
||||
this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
|
||||
this.$removeBranchButton = $('.remove_source_branch');
|
||||
});
|
||||
|
||||
describe('removeSourceBranch', () => {
|
||||
it('shows loader', () => {
|
||||
$('.remove_source_branch').trigger('click');
|
||||
expect(this.$removeBranchProgress).toBeVisible();
|
||||
expect(this.$removeBranchWidget).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeBranchSuccess', () => {
|
||||
it('refreshes page when branch removed', () => {
|
||||
spyOn(gl.utils, 'refreshCurrentPage').and.stub();
|
||||
const response = { status: 200 };
|
||||
this.$removeBranchButton.trigger('ajax:success', response, 'xhr');
|
||||
expect(gl.utils.refreshCurrentPage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeBranchError', () => {
|
||||
it('shows error message', () => {
|
||||
const response = { status: 500 };
|
||||
this.$removeBranchButton.trigger('ajax:error', response, 'xhr');
|
||||
expect(this.$removeBranchFailed).toBeVisible();
|
||||
expect(this.$removeBranchProgress).not.toBeVisible();
|
||||
expect(this.$removeBranchWidget).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,8 +6,8 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
|
|||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
def create_link(data)
|
||||
link_to('text', '', class: 'gfm has-tooltip', data: data)
|
||||
def create_link(text, data)
|
||||
link_to(text, '', class: 'gfm has-tooltip', data: data)
|
||||
end
|
||||
|
||||
it 'ignores non-GFM links' do
|
||||
|
@ -19,16 +19,37 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
|
|||
|
||||
it 'ignores non-issuable links' do
|
||||
project = create(:empty_project, :public)
|
||||
link = create_link(project: project, reference_type: 'issue')
|
||||
link = create_link('text', project: project, reference_type: 'issue')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.text).to eq('text')
|
||||
end
|
||||
|
||||
it 'ignores issuable links with empty content' do
|
||||
issue = create(:issue, :closed)
|
||||
link = create_link('', issue: issue.id, reference_type: 'issue')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.text).to eq('')
|
||||
end
|
||||
|
||||
it 'adds text with standard formatting' do
|
||||
issue = create(:issue, :closed)
|
||||
link = create_link(
|
||||
'something <strong>else</strong>'.html_safe,
|
||||
issue: issue.id,
|
||||
reference_type: 'issue'
|
||||
)
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.inner_html).
|
||||
to eq('something <strong>else</strong> [closed]')
|
||||
end
|
||||
|
||||
context 'for issue references' do
|
||||
it 'ignores open issue references' do
|
||||
issue = create(:issue)
|
||||
link = create_link(issue: issue.id, reference_type: 'issue')
|
||||
link = create_link('text', issue: issue.id, reference_type: 'issue')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.text).to eq('text')
|
||||
|
@ -36,7 +57,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
|
|||
|
||||
it 'ignores reopened issue references' do
|
||||
reopened_issue = create(:issue, :reopened)
|
||||
link = create_link(issue: reopened_issue.id, reference_type: 'issue')
|
||||
link = create_link('text', issue: reopened_issue.id, reference_type: 'issue')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.text).to eq('text')
|
||||
|
@ -44,7 +65,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
|
|||
|
||||
it 'appends [closed] to closed issue references' do
|
||||
closed_issue = create(:issue, :closed)
|
||||
link = create_link(issue: closed_issue.id, reference_type: 'issue')
|
||||
link = create_link('text', issue: closed_issue.id, reference_type: 'issue')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.text).to eq('text [closed]')
|
||||
|
@ -54,7 +75,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
|
|||
context 'for merge request references' do
|
||||
it 'ignores open merge request references' do
|
||||
mr = create(:merge_request)
|
||||
link = create_link(merge_request: mr.id, reference_type: 'merge_request')
|
||||
link = create_link('text', merge_request: mr.id, reference_type: 'merge_request')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.text).to eq('text')
|
||||
|
@ -62,7 +83,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
|
|||
|
||||
it 'ignores reopened merge request references' do
|
||||
mr = create(:merge_request, :reopened)
|
||||
link = create_link(merge_request: mr.id, reference_type: 'merge_request')
|
||||
link = create_link('text', merge_request: mr.id, reference_type: 'merge_request')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.text).to eq('text')
|
||||
|
@ -70,7 +91,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
|
|||
|
||||
it 'ignores locked merge request references' do
|
||||
mr = create(:merge_request, :locked)
|
||||
link = create_link(merge_request: mr.id, reference_type: 'merge_request')
|
||||
link = create_link('text', merge_request: mr.id, reference_type: 'merge_request')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.text).to eq('text')
|
||||
|
@ -78,7 +99,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
|
|||
|
||||
it 'appends [closed] to closed merge request references' do
|
||||
mr = create(:merge_request, :closed)
|
||||
link = create_link(merge_request: mr.id, reference_type: 'merge_request')
|
||||
link = create_link('text', merge_request: mr.id, reference_type: 'merge_request')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.text).to eq('text [closed]')
|
||||
|
@ -86,7 +107,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
|
|||
|
||||
it 'appends [merged] to merged merge request references' do
|
||||
mr = create(:merge_request, :merged)
|
||||
link = create_link(merge_request: mr.id, reference_type: 'merge_request')
|
||||
link = create_link('text', merge_request: mr.id, reference_type: 'merge_request')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').last.text).to eq('text [merged]')
|
||||
|
|
|
@ -33,10 +33,20 @@ describe ContainerRegistry::Path do
|
|||
end
|
||||
|
||||
describe '#to_s' do
|
||||
let(:path) { 'some/image' }
|
||||
context 'when path does not have uppercase characters' do
|
||||
let(:path) { 'some/image' }
|
||||
|
||||
it 'return a string with a repository path' do
|
||||
expect(subject.to_s).to eq path
|
||||
it 'return a string with a repository path' do
|
||||
expect(subject.to_s).to eq 'some/image'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when path has uppercase characters' do
|
||||
let(:path) { 'SoMe/ImAgE' }
|
||||
|
||||
it 'return a string with a repository path' do
|
||||
expect(subject.to_s).to eq 'some/image'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -70,6 +80,12 @@ describe ContainerRegistry::Path do
|
|||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'when path contains uppercase letters' do
|
||||
let(:path) { 'Some/Registry' }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_repository?' do
|
||||
|
|
|
@ -17,12 +17,12 @@ describe Gitlab::Ci::Trace::Stream do
|
|||
describe '#limit' do
|
||||
let(:stream) do
|
||||
described_class.new do
|
||||
StringIO.new("12345678")
|
||||
StringIO.new((1..8).to_a.join("\n"))
|
||||
end
|
||||
end
|
||||
|
||||
it 'if size is larger we start from beggining' do
|
||||
stream.limit(10)
|
||||
it 'if size is larger we start from beginning' do
|
||||
stream.limit(20)
|
||||
|
||||
expect(stream.tell).to eq(0)
|
||||
end
|
||||
|
@ -30,7 +30,27 @@ describe Gitlab::Ci::Trace::Stream do
|
|||
it 'if size is smaller we start from the end' do
|
||||
stream.limit(2)
|
||||
|
||||
expect(stream.tell).to eq(6)
|
||||
expect(stream.raw).to eq("8")
|
||||
end
|
||||
|
||||
context 'when the trace contains ANSI sequence and Unicode' do
|
||||
let(:stream) do
|
||||
described_class.new do
|
||||
File.open(expand_fixture_path('trace/ansi-sequence-and-unicode'))
|
||||
end
|
||||
end
|
||||
|
||||
it 'forwards to the next linefeed, case 1' do
|
||||
stream.limit(7)
|
||||
|
||||
expect(stream.raw).to eq('')
|
||||
end
|
||||
|
||||
it 'forwards to the next linefeed, case 2' do
|
||||
stream.limit(29)
|
||||
|
||||
expect(stream.raw).to eq("\e[01;32m許功蓋\e[0m\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -34,8 +34,18 @@ describe ContainerRepository do
|
|||
end
|
||||
|
||||
describe '#path' do
|
||||
it 'returns a full path to the repository' do
|
||||
expect(repository.path).to eq('group/test/my_image')
|
||||
context 'when project path does not contain uppercase letters' do
|
||||
it 'returns a full path to the repository' do
|
||||
expect(repository.path).to eq('group/test/my_image')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when path contains uppercase letters' do
|
||||
let(:project) { create(:project, path: 'MY_PROJECT', group: group) }
|
||||
|
||||
it 'returns a full path without capital letters' do
|
||||
expect(repository.path).to eq('group/my_project/my_image')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -144,6 +144,20 @@ describe Projects::CreateService, '#execute', services: true do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when a bad service template is created' do
|
||||
before do
|
||||
create(:service, type: 'DroneCiService', project: nil, template: true, active: true)
|
||||
end
|
||||
|
||||
it 'reports an error in the imported project' do
|
||||
opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce'
|
||||
project = create_project(user, opts)
|
||||
|
||||
expect(project.errors.full_messages_for(:base).first).to match /Unable to save project. Error: Unable to save DroneCiService/
|
||||
expect(project.services.count).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
def create_project(user, opts)
|
||||
Projects::CreateService.new(user, opts).execute
|
||||
end
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Users::BuildService, services: true do
|
||||
describe '#execute' do
|
||||
let(:params) do
|
||||
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
|
||||
end
|
||||
|
||||
context 'with an admin user' do
|
||||
let(:admin_user) { create(:admin) }
|
||||
let(:service) { described_class.new(admin_user, params) }
|
||||
|
||||
it 'returns a valid user' do
|
||||
expect(service.execute).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non admin user' do
|
||||
let(:user) { create(:user) }
|
||||
let(:service) { described_class.new(user, params) }
|
||||
|
||||
it 'raises AccessDeniedError exception' do
|
||||
expect { service.execute }.to raise_error Gitlab::Access::AccessDeniedError
|
||||
end
|
||||
end
|
||||
|
||||
context 'with nil user' do
|
||||
let(:service) { described_class.new(nil, params) }
|
||||
|
||||
it 'returns a valid user' do
|
||||
expect(service.execute).to be_valid
|
||||
end
|
||||
|
||||
context 'when "send_user_confirmation_email" application setting is true' do
|
||||
before do
|
||||
stub_application_setting(send_user_confirmation_email: true, signup_enabled?: true)
|
||||
end
|
||||
|
||||
it 'does not confirm the user' do
|
||||
expect(service.execute).not_to be_confirmed
|
||||
end
|
||||
end
|
||||
|
||||
context 'when "send_user_confirmation_email" application setting is false' do
|
||||
before do
|
||||
stub_application_setting(send_user_confirmation_email: false, signup_enabled?: true)
|
||||
end
|
||||
|
||||
it 'confirms the user' do
|
||||
expect(service.execute).to be_confirmed
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,38 +1,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Users::CreateService, services: true do
|
||||
describe '#build' do
|
||||
let(:params) do
|
||||
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
|
||||
end
|
||||
|
||||
context 'with an admin user' do
|
||||
let(:admin_user) { create(:admin) }
|
||||
let(:service) { described_class.new(admin_user, params) }
|
||||
|
||||
it 'returns a valid user' do
|
||||
expect(service.build).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non admin user' do
|
||||
let(:user) { create(:user) }
|
||||
let(:service) { described_class.new(user, params) }
|
||||
|
||||
it 'raises AccessDeniedError exception' do
|
||||
expect { service.build }.to raise_error Gitlab::Access::AccessDeniedError
|
||||
end
|
||||
end
|
||||
|
||||
context 'with nil user' do
|
||||
let(:service) { described_class.new(nil, params) }
|
||||
|
||||
it 'returns a valid user' do
|
||||
expect(service.build).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let(:admin_user) { create(:admin) }
|
||||
|
||||
|
@ -185,40 +153,18 @@ describe Users::CreateService, services: true do
|
|||
end
|
||||
let(:service) { described_class.new(nil, params) }
|
||||
|
||||
context 'when "send_user_confirmation_email" application setting is true' do
|
||||
before do
|
||||
current_application_settings = double(:current_application_settings, send_user_confirmation_email: true, signup_enabled?: true)
|
||||
allow(service).to receive(:current_application_settings).and_return(current_application_settings)
|
||||
end
|
||||
it 'persists the given attributes' do
|
||||
user = service.execute
|
||||
user.reload
|
||||
|
||||
it 'does not confirm the user' do
|
||||
expect(service.execute).not_to be_confirmed
|
||||
end
|
||||
end
|
||||
|
||||
context 'when "send_user_confirmation_email" application setting is false' do
|
||||
before do
|
||||
current_application_settings = double(:current_application_settings, send_user_confirmation_email: false, signup_enabled?: true)
|
||||
allow(service).to receive(:current_application_settings).and_return(current_application_settings)
|
||||
end
|
||||
|
||||
it 'confirms the user' do
|
||||
expect(service.execute).to be_confirmed
|
||||
end
|
||||
|
||||
it 'persists the given attributes' do
|
||||
user = service.execute
|
||||
user.reload
|
||||
|
||||
expect(user).to have_attributes(
|
||||
name: params[:name],
|
||||
username: params[:username],
|
||||
email: params[:email],
|
||||
password: params[:password],
|
||||
created_by_id: nil,
|
||||
admin: false
|
||||
)
|
||||
end
|
||||
expect(user).to have_attributes(
|
||||
name: params[:name],
|
||||
username: params[:username],
|
||||
email: params[:email],
|
||||
password: params[:password],
|
||||
created_by_id: nil,
|
||||
admin: false
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,8 +9,14 @@ require 'rspec/rails'
|
|||
require 'shoulda/matchers'
|
||||
require 'rspec/retry'
|
||||
|
||||
if (ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING']) &&
|
||||
(!ENV.has_key?('CI') || ENV['CI_COMMIT_REF_NAME'] == 'master')
|
||||
rspec_profiling_is_configured =
|
||||
ENV['RSPEC_PROFILING_POSTGRES_URL'] ||
|
||||
ENV['RSPEC_PROFILING']
|
||||
branch_can_be_profiled =
|
||||
ENV['CI_COMMIT_REF_NAME'] == 'master' ||
|
||||
ENV['CI_COMMIT_REF_NAME'] =~ /rspec-profile/
|
||||
|
||||
if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled)
|
||||
require 'rspec_profiling/rspec'
|
||||
end
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
module FixtureHelpers
|
||||
def fixture_file(filename)
|
||||
return '' if filename.blank?
|
||||
file_path = File.expand_path(Rails.root.join('spec/fixtures/', filename))
|
||||
File.read(file_path)
|
||||
File.read(expand_fixture_path(filename))
|
||||
end
|
||||
|
||||
def expand_fixture_path(filename)
|
||||
File.expand_path(Rails.root.join('spec/fixtures/', filename))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ describe 'gitlab:gitaly namespace rake task' do
|
|||
describe 'install' do
|
||||
let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' }
|
||||
let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s }
|
||||
let(:tag) { "v#{File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp}" }
|
||||
let(:version) { File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp }
|
||||
|
||||
context 'no dir given' do
|
||||
it 'aborts and display a help message' do
|
||||
|
@ -21,7 +21,7 @@ describe 'gitlab:gitaly namespace rake task' do
|
|||
context 'when an underlying Git command fail' do
|
||||
it 'aborts and display a help message' do
|
||||
expect_any_instance_of(Object).
|
||||
to receive(:checkout_or_clone_tag).and_raise 'Git error'
|
||||
to receive(:checkout_or_clone_version).and_raise 'Git error'
|
||||
|
||||
expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error'
|
||||
end
|
||||
|
@ -32,9 +32,9 @@ describe 'gitlab:gitaly namespace rake task' do
|
|||
expect(Dir).to receive(:chdir).with(clone_path)
|
||||
end
|
||||
|
||||
it 'calls checkout_or_clone_tag with the right arguments' do
|
||||
it 'calls checkout_or_clone_version with the right arguments' do
|
||||
expect_any_instance_of(Object).
|
||||
to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path)
|
||||
to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path)
|
||||
|
||||
run_rake_task('gitlab:gitaly:install', clone_path)
|
||||
end
|
||||
|
@ -48,7 +48,7 @@ describe 'gitlab:gitaly namespace rake task' do
|
|||
|
||||
context 'gmake is available' do
|
||||
before do
|
||||
expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
|
||||
expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
|
||||
allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
|
||||
end
|
||||
|
||||
|
@ -62,7 +62,7 @@ describe 'gitlab:gitaly namespace rake task' do
|
|||
|
||||
context 'gmake is not available' do
|
||||
before do
|
||||
expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
|
||||
expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
|
||||
allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
|
||||
end
|
||||
|
||||
|
|
|
@ -10,19 +10,38 @@ describe Gitlab::TaskHelpers do
|
|||
|
||||
let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-test.git' }
|
||||
let(:clone_path) { Rails.root.join('tmp/tests/task_helpers_tests').to_s }
|
||||
let(:version) { '1.1.0' }
|
||||
let(:tag) { 'v1.1.0' }
|
||||
|
||||
describe '#checkout_or_clone_tag' do
|
||||
describe '#checkout_or_clone_version' do
|
||||
before do
|
||||
allow(subject).to receive(:run_command!)
|
||||
expect(subject).to receive(:reset_to_tag).with(tag, clone_path)
|
||||
end
|
||||
|
||||
context 'target_dir does not exist' do
|
||||
it 'clones the repo, retrieve the tag from origin, and checkout the tag' do
|
||||
it 'checkout the version and reset to it' do
|
||||
expect(subject).to receive(:checkout_version).with(tag, clone_path)
|
||||
expect(subject).to receive(:reset_to_version).with(tag, clone_path)
|
||||
|
||||
subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
|
||||
end
|
||||
|
||||
context 'with a branch version' do
|
||||
let(:version) { '=branch_name' }
|
||||
let(:branch) { 'branch_name' }
|
||||
|
||||
it 'checkout the version and reset to it with a branch name' do
|
||||
expect(subject).to receive(:checkout_version).with(branch, clone_path)
|
||||
expect(subject).to receive(:reset_to_version).with(branch, clone_path)
|
||||
|
||||
subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
|
||||
end
|
||||
end
|
||||
|
||||
context "target_dir doesn't exist" do
|
||||
it 'clones the repo' do
|
||||
expect(subject).to receive(:clone_repo).with(repo, clone_path)
|
||||
|
||||
subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path)
|
||||
subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -31,10 +50,10 @@ describe Gitlab::TaskHelpers do
|
|||
expect(Dir).to receive(:exist?).and_return(true)
|
||||
end
|
||||
|
||||
it 'fetch and checkout the tag' do
|
||||
expect(subject).to receive(:checkout_tag).with(tag, clone_path)
|
||||
it "doesn't clone the repository" do
|
||||
expect(subject).not_to receive(:clone_repo)
|
||||
|
||||
subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path)
|
||||
subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -48,49 +67,23 @@ describe Gitlab::TaskHelpers do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#checkout_tag' do
|
||||
describe '#checkout_version' do
|
||||
it 'clones the repo in the target dir' do
|
||||
expect(subject).
|
||||
to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --tags --quiet])
|
||||
to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --quiet])
|
||||
expect(subject).
|
||||
to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} checkout --quiet #{tag}])
|
||||
|
||||
subject.checkout_tag(tag, clone_path)
|
||||
subject.checkout_version(tag, clone_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reset_to_tag' do
|
||||
let(:tag) { 'v1.1.0' }
|
||||
before do
|
||||
describe '#reset_to_version' do
|
||||
it 'resets --hard to the given version' do
|
||||
expect(subject).
|
||||
to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} reset --hard #{tag}])
|
||||
end
|
||||
|
||||
context 'when the tag is not checked out locally' do
|
||||
before do
|
||||
expect(subject).
|
||||
to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_raise(Gitlab::TaskFailedError)
|
||||
end
|
||||
|
||||
it 'fetch origin, ensure the tag exists, and resets --hard to the given tag' do
|
||||
expect(subject).
|
||||
to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch origin])
|
||||
expect(subject).
|
||||
to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- origin/#{tag}]).and_return(tag)
|
||||
|
||||
subject.reset_to_tag(tag, clone_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the tag is checked out locally' do
|
||||
before do
|
||||
expect(subject).
|
||||
to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_return(tag)
|
||||
end
|
||||
|
||||
it 'resets --hard to the given tag' do
|
||||
subject.reset_to_tag(tag, clone_path)
|
||||
end
|
||||
subject.reset_to_version(tag, clone_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ describe 'gitlab:workhorse namespace rake task' do
|
|||
describe 'install' do
|
||||
let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' }
|
||||
let(:clone_path) { Rails.root.join('tmp/tests/gitlab-workhorse').to_s }
|
||||
let(:tag) { "v#{File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp}" }
|
||||
let(:version) { File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp }
|
||||
|
||||
context 'no dir given' do
|
||||
it 'aborts and display a help message' do
|
||||
|
@ -21,7 +21,7 @@ describe 'gitlab:workhorse namespace rake task' do
|
|||
context 'when an underlying Git command fail' do
|
||||
it 'aborts and display a help message' do
|
||||
expect_any_instance_of(Object).
|
||||
to receive(:checkout_or_clone_tag).and_raise 'Git error'
|
||||
to receive(:checkout_or_clone_version).and_raise 'Git error'
|
||||
|
||||
expect { run_rake_task('gitlab:workhorse:install', clone_path) }.to raise_error 'Git error'
|
||||
end
|
||||
|
@ -32,9 +32,9 @@ describe 'gitlab:workhorse namespace rake task' do
|
|||
expect(Dir).to receive(:chdir).with(clone_path)
|
||||
end
|
||||
|
||||
it 'calls checkout_or_clone_tag with the right arguments' do
|
||||
it 'calls checkout_or_clone_version with the right arguments' do
|
||||
expect_any_instance_of(Object).
|
||||
to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path)
|
||||
to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path)
|
||||
|
||||
run_rake_task('gitlab:workhorse:install', clone_path)
|
||||
end
|
||||
|
@ -48,7 +48,7 @@ describe 'gitlab:workhorse namespace rake task' do
|
|||
|
||||
context 'gmake is available' do
|
||||
before do
|
||||
expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
|
||||
expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
|
||||
allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
|
||||
end
|
||||
|
||||
|
@ -62,7 +62,7 @@ describe 'gitlab:workhorse namespace rake task' do
|
|||
|
||||
context 'gmake is not available' do
|
||||
before do
|
||||
expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
|
||||
expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
|
||||
allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
|
||||
end
|
||||
|
||||
|
|
|
@ -699,6 +699,48 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
|
|||
//
|
||||
//
|
||||
|
||||
var renderer = new _marked2.default.Renderer();
|
||||
|
||||
/*
|
||||
Regex to match KaTex blocks.
|
||||
|
||||
Supports the following:
|
||||
|
||||
\begin{equation}<math>\end{equation}
|
||||
$$<math>$$
|
||||
inline $<math>$
|
||||
|
||||
The matched text then goes through the KaTex renderer & then outputs the HTML
|
||||
*/
|
||||
var katexRegexString = '(\n ^\\\\begin{[a-zA-Z]+}\\s\n |\n ^\\$\\$\n |\n \\s\\$(?!\\$)\n)\n (.+?)\n(\n \\s\\\\end{[a-zA-Z]+}$\n |\n \\$\\$$\n |\n \\$\n)\n'.replace(/\s/g, '').trim();
|
||||
|
||||
renderer.paragraph = function (t) {
|
||||
var text = t;
|
||||
var inline = false;
|
||||
|
||||
if (typeof katex !== 'undefined') {
|
||||
var katexString = text.replace(/\\/g, '\\');
|
||||
var matches = new RegExp(katexRegexString, 'gi').exec(katexString);
|
||||
|
||||
if (matches && matches.length > 0) {
|
||||
if (matches[1].trim() === '$' && matches[3].trim() === '$') {
|
||||
inline = true;
|
||||
|
||||
text = katexString.replace(matches[0], '') + ' ' + katex.renderToString(matches[2]);
|
||||
} else {
|
||||
text = katex.renderToString(matches[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '<p class="' + (inline ? 'inline-katex' : '') + '">' + text + '</p>';
|
||||
};
|
||||
|
||||
_marked2.default.setOptions({
|
||||
sanitize: true,
|
||||
renderer: renderer
|
||||
});
|
||||
|
||||
exports.default = {
|
||||
components: {
|
||||
prompt: _prompt2.default
|
||||
|
@ -711,20 +753,7 @@ exports.default = {
|
|||
},
|
||||
computed: {
|
||||
markdown: function markdown() {
|
||||
var regex = new RegExp('^\\$\\$(.*)\\$\\$$', 'g');
|
||||
|
||||
var source = this.cell.source.map(function (line) {
|
||||
var matches = regex.exec(line.trim());
|
||||
|
||||
// Only render use the Katex library if it is actually loaded
|
||||
if (matches && matches.length > 0 && typeof katex !== 'undefined') {
|
||||
return katex.renderToString(matches[1]);
|
||||
}
|
||||
|
||||
return line;
|
||||
});
|
||||
|
||||
return (0, _marked2.default)(source.join(''));
|
||||
return (0, _marked2.default)(this.cell.source.join(''));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -3047,7 +3076,7 @@ exports = module.exports = __webpack_require__(1)(undefined);
|
|||
|
||||
|
||||
// module
|
||||
exports.push([module.i, ".markdown .katex{display:block;text-align:center}", ""]);
|
||||
exports.push([module.i, ".markdown .katex{display:block;text-align:center}.markdown .inline-katex .katex{display:inline;text-align:initial}", ""]);
|
||||
|
||||
// exports
|
||||
|
||||
|
|
Loading…
Reference in New Issue