Commit 160157a9 authored by Mike Greiling's avatar Mike Greiling

Prettify remaining files with differences in CE and EE

parent ed816c3d
...@@ -13,7 +13,7 @@ export default () => { ...@@ -13,7 +13,7 @@ export default () => {
if (editBlobForm.length) { if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot'); const urlRoot = editBlobForm.data('relativeUrlRoot');
const assetsPath = editBlobForm.data('assetsPrefix'); const assetsPath = editBlobForm.data('assetsPrefix');
const filePath = editBlobForm.data('blobFilename') const filePath = editBlobForm.data('blobFilename');
const currentAction = $('.js-file-title').data('currentAction'); const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id'); const projectId = editBlobForm.data('project-id');
......
...@@ -42,7 +42,7 @@ export default Vue.extend({ ...@@ -42,7 +42,7 @@ export default Vue.extend({
required: true, required: true,
}, },
}, },
data () { data() {
return { return {
detailIssue: boardsStore.detail, detailIssue: boardsStore.detail,
filter: boardsStore.filter, filter: boardsStore.filter,
...@@ -55,27 +55,26 @@ export default Vue.extend({ ...@@ -55,27 +55,26 @@ export default Vue.extend({
}, },
isNewIssueShown() { isNewIssueShown() {
return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed'); return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed');
} },
}, },
watch: { watch: {
filter: { filter: {
handler() { handler() {
this.list.page = 1; this.list.page = 1;
this.list.getIssues(true) this.list.getIssues(true).catch(() => {
.catch(() => { // TODO: handle request error
// TODO: handle request error });
});
}, },
deep: true, deep: true,
} },
}, },
mounted () { mounted() {
this.sortableOptions = getBoardSortableDefaultOptions({ this.sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
group: 'boards', group: 'boards',
draggable: '.is-draggable', draggable: '.is-draggable',
handle: '.js-board-handle', handle: '.js-board-handle',
onEnd: (e) => { onEnd: e => {
sortableEnd(); sortableEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
...@@ -86,14 +85,15 @@ export default Vue.extend({ ...@@ -86,14 +85,15 @@ export default Vue.extend({
boardsStore.moveList(list, order); boardsStore.moveList(list, order);
}); });
} }
} },
}); });
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
}, },
created() { created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) { if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false'; const isCollapsed =
localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed; this.list.isExpanded = !isCollapsed;
} }
...@@ -107,7 +107,10 @@ export default Vue.extend({ ...@@ -107,7 +107,10 @@ export default Vue.extend({
this.list.isExpanded = !this.list.isExpanded; this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) { if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); localStorage.setItem(
`boards.${this.boardId}.${this.list.type}.expanded`,
this.list.isExpanded,
);
} }
} }
}, },
......
...@@ -32,18 +32,18 @@ export default { ...@@ -32,18 +32,18 @@ export default {
boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position'); boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position');
// Save the labels // Save the labels
gl.boardService.generateDefaultLists() gl.boardService
.generateDefaultLists()
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
data.forEach((listObj) => { data.forEach(listObj => {
const list = boardsStore.findList('title', listObj.title); const list = boardsStore.findList('title', listObj.title);
list.id = listObj.id; list.id = listObj.id;
list.label.id = listObj.label.id; list.label.id = listObj.label.id;
list.getIssues() list.getIssues().catch(() => {
.catch(() => { // TODO: handle request error
// TODO: handle request error });
});
}); });
}) })
.catch(() => { .catch(() => {
...@@ -57,7 +57,6 @@ export default { ...@@ -57,7 +57,6 @@ export default {
clearBlankState: boardsStore.removeBlankState.bind(boardsStore), clearBlankState: boardsStore.removeBlankState.bind(boardsStore),
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue'; import IssueCardInner from './issue_card_inner.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
export default { export default {
name: 'BoardsIssueCard', name: 'BoardsIssueCard',
components: { components: {
IssueCardInner, IssueCardInner,
},
props: {
list: {
type: Object,
default: () => ({}),
}, },
props: { issue: {
list: { type: Object,
type: Object, default: () => ({}),
default: () => ({}),
},
issue: {
type: Object,
default: () => ({}),
},
issueLinkBase: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
index: {
type: Number,
default: 0,
},
rootPath: {
type: String,
default: '',
},
groupId: {
type: Number,
},
}, },
data() { issueLinkBase: {
return { type: String,
showDetail: false, default: '',
detailIssue: boardsStore.detail,
};
}, },
computed: { disabled: {
issueDetailVisible() { type: Boolean,
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; default: false,
},
}, },
methods: { index: {
mouseDown() { type: Number,
this.showDetail = true; default: 0,
}, },
mouseMove() { rootPath: {
this.showDetail = false; type: String,
}, default: '',
showIssue(e) { },
if (e.target.classList.contains('js-no-trigger')) return; groupId: {
type: Number,
},
},
data() {
return {
showDetail: false,
detailIssue: boardsStore.detail,
};
},
computed: {
issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
},
},
methods: {
mouseDown() {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) { if (this.showDetail) {
this.showDetail = false; this.showDetail = false;
if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
eventHub.$emit('clearDetailIssue'); eventHub.$emit('clearDetailIssue');
} else { } else {
eventHub.$emit('newDetailIssue', this.issue); eventHub.$emit('newDetailIssue', this.issue);
boardsStore.detail.list = this.list; boardsStore.detail.list = this.list;
}
} }
}, }
}, },
}; },
};
</script> </script>
<template> <template>
......
...@@ -62,7 +62,8 @@ export default { ...@@ -62,7 +62,8 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`); eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel(); this.cancel();
return this.list.newIssue(issue) return this.list
.newIssue(issue)
.then(() => { .then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
......
...@@ -38,7 +38,7 @@ export default Vue.extend({ ...@@ -38,7 +38,7 @@ export default Vue.extend({
}; };
}, },
computed: { computed: {
showSidebar () { showSidebar() {
return Object.keys(this.issue).length; return Object.keys(this.issue).length;
}, },
milestoneTitle() { milestoneTitle() {
...@@ -51,18 +51,20 @@ export default Vue.extend({ ...@@ -51,18 +51,20 @@ export default Vue.extend({
return this.issue.labels && this.issue.labels.length; return this.issue.labels && this.issue.labels.length;
}, },
labelDropdownTitle() { labelDropdownTitle() {
return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), { return this.hasLabels
firstLabel: this.issue.labels[0].title, ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
labelCount: this.issue.labels.length - 1 firstLabel: this.issue.labels[0].title,
}) : __('Label'); labelCount: this.issue.labels.length - 1,
})
: __('Label');
}, },
selectedLabels() { selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
} },
}, },
watch: { watch: {
detail: { detail: {
handler () { handler() {
if (this.issue.id !== this.detail.issue.id) { if (this.issue.id !== this.detail.issue.id) {
$('.block.assignee') $('.block.assignee')
.find('input:not(.js-vue)[name="issue[assignee_ids][]"]') .find('input:not(.js-vue)[name="issue[assignee_ids][]"]')
...@@ -71,17 +73,19 @@ export default Vue.extend({ ...@@ -71,17 +73,19 @@ export default Vue.extend({
}); });
$('.js-issue-board-sidebar', this.$el).each((i, el) => { $('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el).data('glDropdown').clearMenu(); $(el)
.data('glDropdown')
.clearMenu();
}); });
} }
this.issue = this.detail.issue; this.issue = this.detail.issue;
this.list = this.detail.list; this.list = this.detail.list;
}, },
deep: true deep: true,
}, },
}, },
created () { created() {
// Get events from glDropdown // Get events from glDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee); eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee); eventHub.$on('sidebar.addAssignee', this.addAssignee);
...@@ -94,7 +98,7 @@ export default Vue.extend({ ...@@ -94,7 +98,7 @@ export default Vue.extend({
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees); eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
}, },
mounted () { mounted() {
new IssuableContext(this.currentUser); new IssuableContext(this.currentUser);
new MilestoneSelect(); new MilestoneSelect();
new DueDateSelectors(); new DueDateSelectors();
...@@ -102,29 +106,30 @@ export default Vue.extend({ ...@@ -102,29 +106,30 @@ export default Vue.extend({
new Sidebar(); new Sidebar();
}, },
methods: { methods: {
closeSidebar () { closeSidebar() {
this.detail.issue = {}; this.detail.issue = {};
}, },
assignSelf () { assignSelf() {
// Notify gl dropdown that we are now assigning to current user // Notify gl dropdown that we are now assigning to current user
this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself')); this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself'));
this.addAssignee(this.currentUser); this.addAssignee(this.currentUser);
this.saveAssignees(); this.saveAssignees();
}, },
removeAssignee (a) { removeAssignee(a) {
boardsStore.detail.issue.removeAssignee(a); boardsStore.detail.issue.removeAssignee(a);
}, },
addAssignee (a) { addAssignee(a) {
boardsStore.detail.issue.addAssignee(a); boardsStore.detail.issue.addAssignee(a);
}, },
removeAllAssignees () { removeAllAssignees() {
boardsStore.detail.issue.removeAllAssignees(); boardsStore.detail.issue.removeAllAssignees();
}, },
saveAssignees () { saveAssignees() {
this.loadingAssignees = true; this.loadingAssignees = true;
boardsStore.detail.issue.update() boardsStore.detail.issue
.update()
.then(() => { .then(() => {
this.loadingAssignees = false; this.loadingAssignees = false;
}) })
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
export default { export default {
components: { components: {
UserAvatarLink, UserAvatarLink,
Icon, Icon,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, 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,
},
groupId: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
limitBeforeCounter: 3,
maxRender: 4,
maxCounter: 99,
};
}, },
computed: { issueLinkBase: {
numberOverLimit() { type: String,
return this.issue.assignees.length - this.limitBeforeCounter; required: true,
}, },
assigneeCounterTooltip() { list: {
return `${this.assigneeCounterLabel} more`; type: Object,
}, required: false,
assigneeCounterLabel() { default: () => ({}),
if (this.numberOverLimit > this.maxCounter) { },
return `${this.maxCounter}+`; rootPath: {
} type: String,
required: true,
return `+${this.numberOverLimit}`; },
}, updateFilters: {
shouldRenderCounter() { type: Boolean,
if (this.issue.assignees.length <= this.maxRender) { required: false,
return false; default: false,
} },
groupId: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
limitBeforeCounter: 3,
maxRender: 4,
maxCounter: 99,
};
},
computed: {
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
return `${this.assigneeCounterLabel} more`;
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
return `${this.maxCounter}+`;
}
return this.issue.assignees.length > this.numberOverLimit; return `+${this.numberOverLimit}`;
}, },
issueId() { shouldRenderCounter() {
if (this.issue.iid) { if (this.issue.assignees.length <= this.maxRender) {
return `#${this.issue.iid}`;
}
return false; return false;
}, }
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
},
methods: {
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter; return this.issue.assignees.length > this.numberOverLimit;
}, },
assigneeUrl(assignee) { issueId() {
return `${this.rootPath}${assignee.username}`; if (this.issue.iid) {
}, return `#${this.issue.iid}`;
assigneeUrlTitle(assignee) { }
return `Assigned to ${assignee.name}`; return false;
}, },
avatarUrlTitle(assignee) { showLabelFooter() {
return `Avatar for ${assignee.name}`; return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
}, },
showLabel(label) { },
if (!label.id) return false; methods: {
return true; isIndexLessThanlimit(index) {
}, return index < this.limitBeforeCounter;
filterByLabel(label, e) { },
if (!this.updateFilters) return; shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
return `${this.rootPath}${assignee.username}`;
},
assigneeUrlTitle(assignee) {
return `Assigned to ${assignee.name}`;
},
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
if (!label.id) return false;
return true;
},
filterByLabel(label, e) {
if (!this.updateFilters) return;
const filterPath = boardsStore.filter.path.split('&'); const filterPath = boardsStore.filter.path.split('&');
const labelTitle = encodeURIComponent(label.title); const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`; const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param); const labelIndex = filterPath.indexOf(param);
$(e.currentTarget).tooltip('hide'); $(e.currentTarget).tooltip('hide');
if (labelIndex === -1) { if (labelIndex === -1) {
filterPath.push(param); filterPath.push(param);
} else { } else {
filterPath.splice(labelIndex, 1); filterPath.splice(labelIndex, 1);
} }
boardsStore.filter.path = filterPath.join('&'); boardsStore.filter.path = filterPath.join('&');
boardsStore.updateFiltersUrl(); boardsStore.updateFiltersUrl();
eventHub.$emit('updateTokens'); eventHub.$emit('updateTokens');
}, },
labelStyle(label) { labelStyle(label) {
return { return {
backgroundColor: label.color, backgroundColor: label.color,
color: label.textColor, color: label.textColor,
}; };
}, },
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
......
...@@ -20,7 +20,7 @@ export default { ...@@ -20,7 +20,7 @@ export default {
computed: { computed: {
contents() { contents() {
const obj = { const obj = {
title: 'You haven\'t added any issues to your project yet', title: "You haven't added any issues to your project yet",
content: ` content: `
An issue can be a bug, a todo or a feature request that needs to be 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. discussed in a project. Besides, issues are searchable and filterable.
...@@ -28,7 +28,7 @@ export default { ...@@ -28,7 +28,7 @@ export default {
}; };
if (this.activeTab === 'selected') { if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet'; obj.title = "You haven't selected any issues yet";
obj.content = ` obj.content = `
Go back to <strong>Open issues</strong> and select some issues Go back to <strong>Open issues</strong> and select some issues
to add to your board. to add to your board.
......
...@@ -42,19 +42,17 @@ export default { ...@@ -42,19 +42,17 @@ export default {
const req = this.buildUpdateRequest(list); const req = this.buildUpdateRequest(list);
// Post the data to the backend // Post the data to the backend
gl.boardService gl.boardService.bulkUpdate(issueIds, req).catch(() => {
.bulkUpdate(issueIds, req) Flash(__('Failed to update issues, please try again.'));
.catch(() => {
Flash(__('Failed to update issues, please try again.'));
selectedIssues.forEach((issue) => { selectedIssues.forEach(issue => {
list.removeIssue(issue); list.removeIssue(issue);
list.issuesSize -= 1; list.issuesSize -= 1;
});
}); });
});
// Add the issues on the frontend // Add the issues on the frontend
selectedIssues.forEach((issue) => { selectedIssues.forEach(issue => {
list.addIssue(issue); list.addIssue(issue);
list.issuesSize += 1; list.issuesSize += 1;
}); });
......
<script> <script>
import ModalFilters from './filters'; import ModalFilters from './filters';
import ModalTabs from './tabs.vue'; import ModalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
export default { export default {
components: { components: {
ModalTabs, ModalTabs,
ModalFilters, ModalFilters,
},
mixins: [modalMixin],
props: {
projectId: {
type: Number,
required: true,
}, },
mixins: [modalMixin], milestonePath: {
props: { type: String,
projectId: { required: true,
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
}, },
data() { labelPath: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { },
selectAllText() { data() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { return ModalStore.store;
return 'Select all'; },
} computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return 'Select all';
}
return 'Deselect all'; return 'Deselect all';
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
}, },
methods: { showSearch() {
toggleAll() { return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
this.$refs.selectAllBtn.blur(); },
},
methods: {
toggleAll() {
this.$refs.selectAllBtn.blur();
ModalStore.toggleAll(); ModalStore.toggleAll();
},
}, },
}; },
};
</script> </script>
<template> <template>
<div> <div>
......
<script> <script>
/* global ListIssue */ /* global ListIssue */
import { urlParamsToObject } from '~/lib/utils/common_utils'; import { urlParamsToObject } from '~/lib/utils/common_utils';
import ModalHeader from './header.vue'; import ModalHeader from './header.vue';
import ModalList from './list.vue'; import ModalList from './list.vue';
import ModalFooter from './footer.vue'; import ModalFooter from './footer.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
export default { export default {
components: { components: {
EmptyState, EmptyState,
ModalHeader, ModalHeader,
ModalList, ModalList,
ModalFooter, ModalFooter,
},
props: {
newIssuePath: {
type: String,
required: true,
}, },
props: { emptyStateSvg: {
newIssuePath: { type: String,
type: String, required: true,
required: true,
},
emptyStateSvg: {
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,
},
}, },
data() { issueLinkBase: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { rootPath: {
showList() { type: String,
if (this.activeTab === 'selected') { required: true,
return this.selectedIssues.length > 0; },
} projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0; return this.issuesCount > 0;
}, },
showEmptyState() { showEmptyState() {
if (!this.loading && this.issuesCount === 0) { if (!this.loading && this.issuesCount === 0) {
return true; return true;
} }
return this.activeTab === 'selected' && this.selectedIssues.length === 0; return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
}, },
watch: { },
page() { watch: {
this.loadIssues(); page() {
}, this.loadIssues();
showAddIssuesModal() { },
if (this.showAddIssuesModal && !this.issues.length) { showAddIssuesModal() {
this.loading = true; if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
const loadingDone = () => {
this.loading = false;
};
this.loadIssues()
.then(loadingDone)
.catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
}
},
filter: {
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
const loadingDone = () => { const loadingDone = () => {
this.loading = false; this.filterLoading = false;
}; };
this.loadIssues() this.loadIssues(true)
.then(loadingDone) .then(loadingDone)
.catch(loadingDone); .catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
} }
}, },
filter: { deep: true,
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
const loadingDone = () => {
this.filterLoading = false;
};
this.loadIssues(true)
.then(loadingDone)
.catch(loadingDone);
}
},
deep: true,
},
}, },
created() { },
this.page = 1; created() {
}, this.page = 1;
methods: { },
loadIssues(clearIssues = false) { methods: {
if (!this.showAddIssuesModal) return false; loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
return gl.boardService.getBacklog({ return gl.boardService
.getBacklog({
...urlParamsToObject(this.filter.path), ...urlParamsToObject(this.filter.path),
page: this.page, page: this.page,
per: this.perPage, per: this.perPage,
}) })
.then(res => res.data) .then(res => res.data)
.then(data => { .then(data => {
if (clearIssues) { if (clearIssues) {
this.issues = []; this.issues = [];
} }
data.issues.forEach(issueObj => { data.issues.forEach(issueObj => {
const issue = new ListIssue(issueObj); const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue); const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
issue.selected = !!foundSelectedIssue; issue.selected = !!foundSelectedIssue;
this.issues.push(issue); this.issues.push(issue);
}); });
this.loadingNewPage = false; this.loadingNewPage = false;
if (!this.issuesCount) { if (!this.issuesCount) {
this.issuesCount = data.size; this.issuesCount = data.size;
} }
}) })
.catch(() => { .catch(() => {
// TODO: handle request error // TODO: handle request error
}); });
},
}, },
}; },
};
</script> </script>
<template> <template>
<div <div
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import bp from '../../../breakpoints'; import bp from '../../../breakpoints';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import IssueCardInner from '../issue_card_inner.vue'; import IssueCardInner from '../issue_card_inner.vue';
export default { export default {
components: { components: {
IssueCardInner, IssueCardInner,
Icon, Icon,
},
props: {
issueLinkBase: {
type: String,
required: true,
}, },
props: { rootPath: {
issueLinkBase: { type: String,
type: String, required: true,
required: true,
},
rootPath: {
type: String,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
}, },
data() { emptyStateSvg: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { },
loopIssues() { data() {
if (this.activeTab === 'all') { return ModalStore.store;
return this.issues; },
} computed: {
loopIssues() {
if (this.activeTab === 'all') {
return this.issues;
}
return this.selectedIssues; return this.selectedIssues;
}, },
groupedIssues() { groupedIssues() {
const groups = []; const groups = [];
this.loopIssues.forEach((issue, i) => { this.loopIssues.forEach((issue, i) => {
const index = i % this.columns; const index = i % this.columns;
if (!groups[index]) { if (!groups[index]) {
groups.push([]); groups.push([]);
} }
groups[index].push(issue); groups[index].push(issue);
}); });
return groups; return groups;
},
}, },
watch: { },
activeTab() { watch: {
if (this.activeTab === 'all') { activeTab() {
ModalStore.purgeUnselectedIssues(); if (this.activeTab === 'all') {
} ModalStore.purgeUnselectedIssues();
}, }
}, },
mounted() { },
this.scrollHandlerWrapper = this.scrollHandler.bind(this); mounted() {
this.setColumnCountWrapper = this.setColumnCount.bind(this); this.scrollHandlerWrapper = this.scrollHandler.bind(this);
this.setColumnCount(); this.setColumnCountWrapper = this.setColumnCount.bind(this);
this.setColumnCount();
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
window.addEventListener('resize', this.setColumnCountWrapper); window.addEventListener('resize', this.setColumnCountWrapper);
},
beforeDestroy() {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
window.removeEventListener('resize', this.setColumnCountWrapper);
},
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;
}
}, },
beforeDestroy() { toggleIssue(e, issue) {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); if (e.target.tagName !== 'A') {
window.removeEventListener('resize', this.setColumnCountWrapper); ModalStore.toggleIssue(issue);
}
}, },
methods: { listHeight() {
scrollHandler() { return this.$refs.list.getBoundingClientRect().height;
const currentPage = Math.floor(this.issues.length / this.perPage); },
scrollHeight() {
if ( return this.$refs.list.scrollHeight;
this.scrollTop() > this.scrollHeight() - 100 && },
!this.loadingNewPage && scrollTop() {
currentPage === this.page return this.$refs.list.scrollTop + this.listHeight();
) { },
this.loadingNewPage = true; showIssue(issue) {
this.page += 1; if (this.activeTab === 'all') return true;
}
},
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); const index = ModalStore.selectedIssueIndex(issue);
return index !== -1; return index !== -1;
}, },
setColumnCount() { setColumnCount() {
const breakpoint = bp.getBreakpointSize(); const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'lg' || breakpoint === 'md') { if (breakpoint === 'lg' || breakpoint === 'md') {
this.columns = 3; this.columns = 3;
} else if (breakpoint === 'sm') { } else if (breakpoint === 'sm') {
this.columns = 2; this.columns = 2;
} else { } else {
this.columns = 1; this.columns = 1;
} }
},
}, },
}; },
};
</script> </script>
<template> <template>
<section <section
......
<script> <script>
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
export default { export default {
mixins: [modalMixin], mixins: [modalMixin],
data() { data() {
return ModalStore.store; return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
}, },
computed: { },
selectedCount() { destroyed() {
return ModalStore.selectedCount(); this.activeTab = 'all';
}, },
}, };
destroyed() {
this.activeTab = 'all';
},
};
</script> </script>
<template> <template>
<div class="top-area prepend-top-10 append-bottom-10"> <div class="top-area prepend-top-10 append-bottom-10">
......
...@@ -6,36 +6,41 @@ import _ from 'underscore'; ...@@ -6,36 +6,41 @@ import _ from 'underscore';
import CreateLabelDropdown from '../../create_label'; import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
$(document).off('created.label').on('created.label', (e, label) => { $(document)
boardsStore.new({ .off('created.label')
title: label.title, .on('created.label', (e, label) => {
position: boardsStore.state.lists.length - 2, boardsStore.new({
list_type: 'label',
label: {
id: label.id,
title: label.title, title: label.title,
color: label.color, position: boardsStore.state.lists.length - 2,
}, list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color,
},
});
}); });
});
export default function initNewListDropdown() { export default function initNewListDropdown() {
$('.js-new-board-list').each(function () { $('.js-new-board-list').each(function() {
const $this = $(this); const $this = $(this);
new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath')); new CreateLabelDropdown(
$this.closest('.dropdown').find('.dropdown-new-label'),
$this.data('namespacePath'),
$this.data('projectPath'),
);
$this.glDropdown({ $this.glDropdown({
data(term, callback) { data(term, callback) {
axios.get($this.attr('data-list-labels-path')) axios.get($this.attr('data-list-labels-path')).then(({ data }) => {
.then(({ data }) => { callback(data);
callback(data); });
});
}, },
renderRow (label) { renderRow(label) {
const active = boardsStore.findList('title', label.title); const active = boardsStore.findList('title', label.title);
const $li = $('<li />'); const $li = $('<li />');
const $a = $('<a />', { const $a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''), class: active ? `is-active js-board-list-${active.id}` : '',
text: label.title, text: label.title,
href: '#', href: '#',
}); });
...@@ -53,7 +58,7 @@ export default function initNewListDropdown() { ...@@ -53,7 +58,7 @@ export default function initNewListDropdown() {
selectable: true, selectable: true,
multiSelect: true, multiSelect: true,
containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content', containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
clicked (options) { clicked(options) {
const { e } = options; const { e } = options;
const label = options.selectedObj; const label = options.selectedObj;
e.preventDefault(); e.preventDefault();
......
...@@ -46,7 +46,7 @@ export default { ...@@ -46,7 +46,7 @@ export default {
selectable: true, selectable: true,
data: (term, callback) => { data: (term, callback) => {
this.loading = true; this.loading = true;
return Api.groupProjects(this.groupId, term, {with_issues_enabled: true}, projects => { return Api.groupProjects(this.groupId, term, { with_issues_enabled: true }, projects => {
this.loading = false; this.loading = false;
callback(projects); callback(projects);
}); });
...@@ -54,7 +54,9 @@ export default { ...@@ -54,7 +54,9 @@ export default {
renderRow(project) { renderRow(project) {
return ` return `
<li> <li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}"> <a href='#' class='dropdown-menu-link' data-project-id="${
project.id
}" data-project-name="${project.name}">
${_.escape(project.name)} ${_.escape(project.name)}
</a> </a>
</li> </li>
......
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import boardsStore from '../../stores/boards_store'; import boardsStore from '../../stores/boards_store';
export default Vue.extend({ export default Vue.extend({
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
list: {
type: Object,
required: true,
},
}, },
computed: { list: {
updateUrl() { type: Object,
return this.issue.path; required: true,
},
}, },
methods: { },
removeIssue() { computed: {
const { issue } = this; updateUrl() {
const lists = issue.getLists(); return this.issue.path;
const req = this.buildPatchRequest(issue, lists); },
},
const data = { methods: {
issue: this.seedPatchRequest(issue, req), removeIssue() {
}; const { issue } = this;
const lists = issue.getLists();
const req = this.buildPatchRequest(issue, lists);
if (data.issue.label_ids.length === 0) { const data = {
data.issue.label_ids = ['']; issue: this.seedPatchRequest(issue, req),
} };
// Post the remove data if (data.issue.label_ids.length === 0) {
Vue.http.patch(this.updateUrl, data).catch(() => { data.issue.label_ids = [''];
Flash(__('Failed to remove issue from board, please try again.')); }
lists.forEach(list => { // Post the remove data
list.addIssue(issue); Vue.http.patch(this.updateUrl, data).catch(() => {
}); Flash(__('Failed to remove issue from board, please try again.'));
});
// Remove from the frontend store
lists.forEach(list => { lists.forEach(list => {
list.removeIssue(issue); list.addIssue(issue);
}); });
});
boardsStore.detail.issue = {}; // Remove from the frontend store
}, lists.forEach(list => {
/** list.removeIssue(issue);
* Build the default patch request. });
*/
buildPatchRequest(issue, lists) {
const listLabelIds = lists.map(list => list.label.id);
const labelIds = issue.labels boardsStore.detail.issue = {};
.map(label => label.id) },
.filter(id => !listLabelIds.includes(id)); /**
* Build the default patch request.
*/
buildPatchRequest(issue, lists) {
const listLabelIds = lists.map(list => list.label.id);
const labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
return { return {
label_ids: labelIds, label_ids: labelIds,
}; };
}, },
/** /**
* Seed the given patch request. * Seed the given patch request.
* *
* (This is overridden in EE) * (This is overridden in EE)
*/ */
seedPatchRequest(issue, req) { seedPatchRequest(issue, req) {
return req; return req;
},
}, },
}); },
});
</script> </script>
<template> <template>
<div <div
......
...@@ -32,7 +32,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { ...@@ -32,7 +32,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token'); const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
// Remove all the tokens as they will be replaced by the search manager // Remove all the tokens as they will be replaced by the search manager
[].forEach.call(tokens, (el) => { [].forEach.call(tokens, el => {
el.parentNode.removeChild(el); el.parentNode.removeChild(el);
}); });
...@@ -50,7 +50,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager { ...@@ -50,7 +50,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
canEdit(tokenName, tokenValue) { canEdit(tokenName, tokenValue) {
if (this.cantEdit.includes(tokenName)) return false; if (this.cantEdit.includes(tokenName)) return false;
return this.cantEditWithValue.findIndex(token => token.name === tokenName && return (
token.value === tokenValue) === -1; this.cantEditWithValue.findIndex(
token => token.name === tokenName && token.value === tokenValue,
) === -1
);
} }
} }
...@@ -32,9 +32,9 @@ export default () => { ...@@ -32,9 +32,9 @@ export default () => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
// check for browser back and trigger a hard reload to circumvent browser caching. // check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => { window.addEventListener('pageshow', event => {
const isNavTypeBackForward = window.performance && const isNavTypeBackForward =
window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD; window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
if (event.persisted || isNavTypeBackForward) { if (event.persisted || isNavTypeBackForward) {
window.location.reload(); window.location.reload();
......
...@@ -4,7 +4,8 @@ import $ from 'jquery'; ...@@ -4,7 +4,8 @@ import $ from 'jquery';
import sortableConfig from '../../sortable/sortable_config'; import sortableConfig from '../../sortable/sortable_config';
export function sortableStart() { export function sortableStart() {
$('.has-tooltip').tooltip('hide') $('.has-tooltip')
.tooltip('hide')
.tooltip('disable'); .tooltip('disable');
document.body.classList.add('is-dragging'); document.body.classList.add('is-dragging');
} }
...@@ -15,7 +16,8 @@ export function sortableEnd() { ...@@ -15,7 +16,8 @@ export function sortableEnd() {
} }
export function getBoardSortableDefaultOptions(obj) { export function getBoardSortableDefaultOptions(obj) {
const touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; const touchEnabled =
'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
const defaultSortOptions = Object.assign({}, sortableConfig, { const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn', filter: '.board-delete, .btn',
...@@ -26,6 +28,8 @@ export function getBoardSortableDefaultOptions(obj) { ...@@ -26,6 +28,8 @@ export function getBoardSortableDefaultOptions(obj) {
onEnd: sortableEnd, onEnd: sortableEnd,
}); });
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); Object.keys(obj).forEach(key => {
defaultSortOptions[key] = obj[key];
});
return defaultSortOptions; return defaultSortOptions;
} }
...@@ -9,7 +9,7 @@ import IssueProject from './project'; ...@@ -9,7 +9,7 @@ import IssueProject from './project';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
class ListIssue { class ListIssue {
constructor (obj, defaultAvatar) { constructor(obj, defaultAvatar) {
this.id = obj.id; this.id = obj.id;
this.iid = obj.iid; this.iid = obj.iid;
this.title = obj.title; this.title = obj.title;
...@@ -39,54 +39,54 @@ class ListIssue { ...@@ -39,54 +39,54 @@ class ListIssue {
this.milestone = new ListMilestone(obj.milestone); this.milestone = new ListMilestone(obj.milestone);
} }
obj.labels.forEach((label) => { obj.labels.forEach(label => {
this.labels.push(new ListLabel(label)); this.labels.push(new ListLabel(label));
}); });
this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar)); this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
} }
addLabel (label) { addLabel(label) {
if (!this.findLabel(label)) { if (!this.findLabel(label)) {
this.labels.push(new ListLabel(label)); this.labels.push(new ListLabel(label));
} }
} }
findLabel (findLabel) { findLabel(findLabel) {
return this.labels.filter(label => label.title === findLabel.title)[0]; return this.labels.filter(label => label.title === findLabel.title)[0];
} }
removeLabel (removeLabel) { removeLabel(removeLabel) {
if (removeLabel) { if (removeLabel) {
this.labels = this.labels.filter(label => removeLabel.title !== label.title); this.labels = this.labels.filter(label => removeLabel.title !== label.title);
} }
} }
removeLabels (labels) { removeLabels(labels) {
labels.forEach(this.removeLabel.bind(this)); labels.forEach(this.removeLabel.bind(this));
} }
addAssignee (assignee) { addAssignee(assignee) {
if (!this.findAssignee(assignee)) { if (!this.findAssignee(assignee)) {
this.assignees.push(new ListAssignee(assignee)); this.assignees.push(new ListAssignee(assignee));
} }
} }
findAssignee (findAssignee) { findAssignee(findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0]; return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
} }
removeAssignee (removeAssignee) { removeAssignee(removeAssignee) {
if (removeAssignee) { if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id); this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
} }
} }
removeAllAssignees () { removeAllAssignees() {
this.assignees = []; this.assignees = [];
} }
getLists () { getLists() {
return boardsStore.state.lists.filter(list => list.findIssue(this.id)); return boardsStore.state.lists.filter(list => list.findIssue(this.id));
} }
...@@ -102,14 +102,14 @@ class ListIssue { ...@@ -102,14 +102,14 @@ class ListIssue {
this.isLoading[key] = value; this.isLoading[key] = value;
} }
update () { update() {
const data = { const data = {
issue: { issue: {
milestone_id: this.milestone ? this.milestone.id : null, milestone_id: this.milestone ? this.milestone.id : null,
due_date: this.dueDate, due_date: this.dueDate,
assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0], assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0],
label_ids: this.labels.map((label) => label.id) label_ids: this.labels.map(label => label.id),
} },
}; };
if (!data.issue.label_ids.length) { if (!data.issue.label_ids.length) {
......
...@@ -234,11 +234,11 @@ class List { ...@@ -234,11 +234,11 @@ class List {
}); });
} }
getTypeInfo (type) { getTypeInfo(type) {
return TYPES[type] || {}; return TYPES[type] || {};
} }
onNewIssueResponse (issue, data) { onNewIssueResponse(issue, data) {
issue.id = data.id; issue.id = data.id;
issue.iid = data.iid; issue.iid = data.iid;
issue.project = data.project; issue.project = data.project;
......
...@@ -19,7 +19,9 @@ export default class BoardService { ...@@ -19,7 +19,9 @@ export default class BoardService {
} }
static generateIssuePath(boardId, id) { static generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`; return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
id ? `/${id}` : ''
}`;
} }
all() { all() {
...@@ -54,7 +56,9 @@ export default class BoardService { ...@@ -54,7 +56,9 @@ export default class BoardService {
getIssuesForList(id, filter = {}) { getIssuesForList(id, filter = {}) {
const data = { id }; const data = { id };
Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); Object.keys(filter).forEach(key => {
data[key] = filter[key];
});
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
} }
...@@ -75,7 +79,9 @@ export default class BoardService { ...@@ -75,7 +79,9 @@ export default class BoardService {
} }
getBacklog(data) { getBacklog(data) {
return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`)); return axios.get(
mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`),
);
} }
bulkUpdate(issueIds, extraData = {}) { bulkUpdate(issueIds, extraData = {}) {
......
...@@ -20,20 +20,20 @@ const boardsStore = { ...@@ -20,20 +20,20 @@ const boardsStore = {
issue: {}, issue: {},
list: {}, list: {},
}, },
create () { create() {
this.state.lists = []; this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&'); this.filter.path = getUrlParamsArray().join('&');
this.detail = { this.detail = {
issue: {}, issue: {},
}; };
}, },
addList (listObj, defaultAvatar) { addList(listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar); const list = new List(listObj, defaultAvatar);
this.state.lists.push(list); this.state.lists.push(list);
return list; return list;
}, },
new (listObj) { new(listObj) {
const list = this.addList(listObj); const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog'); const backlogList = this.findList('type', 'backlog', 'backlog');
...@@ -50,44 +50,44 @@ const boardsStore = { ...@@ -50,44 +50,44 @@ const boardsStore = {
}); });
this.removeBlankState(); this.removeBlankState();
}, },
updateNewListDropdown (listId) { updateNewListDropdown(listId) {
$(`.js-board-list-${listId}`).removeClass('is-active'); $(`.js-board-list-${listId}`).removeClass('is-active');
}, },
shouldAddBlankState () { shouldAddBlankState() {
// Decide whether to add the blank state // Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]); return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0];
}, },
addBlankState () { addBlankState() {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
this.addList({ this.addList({
id: 'blank', id: 'blank',
list_type: 'blank', list_type: 'blank',
title: 'Welcome to your Issue Board!', title: 'Welcome to your Issue Board!',
position: 0 position: 0,
}); });
this.state.lists = _.sortBy(this.state.lists, 'position'); this.state.lists = _.sortBy(this.state.lists, 'position');
}, },
removeBlankState () { removeBlankState() {
this.removeList('blank'); this.removeList('blank');
Cookies.set('issue_board_welcome_hidden', 'true', { Cookies.set('issue_board_welcome_hidden', 'true', {
expires: 365 * 10, expires: 365 * 10,
path: '' path: '',
}); });
}, },
welcomeIsHidden () { welcomeIsHidden() {
return Cookies.get('issue_board_welcome_hidden') === 'true'; return Cookies.get('issue_board_welcome_hidden') === 'true';
}, },
removeList (id, type = 'blank') { removeList(id, type = 'blank') {
const list = this.findList('id', id, type); const list = this.findList('id', id, type);
if (!list) return; if (!list) return;
this.state.lists = this.state.lists.filter(list => list.id !== id); this.state.lists = this.state.lists.filter(list => list.id !== id);
}, },
moveList (listFrom, orderLists) { moveList(listFrom, orderLists) {
orderLists.forEach((id, i) => { orderLists.forEach((id, i) => {
const list = this.findList('id', parseInt(id, 10)); const list = this.findList('id', parseInt(id, 10));
...@@ -95,22 +95,25 @@ const boardsStore = { ...@@ -95,22 +95,25 @@ const boardsStore = {
}); });
listFrom.update(); listFrom.update();
}, },
moveIssueToList (listFrom, listTo, issue, newIndex) { moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id); const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists(); const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label); const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) { if (!issueTo) {
// Check if target list assignee is already present in this issue // Check if target list assignee is already present in this issue
if ((listTo.type === 'assignee' && listFrom.type === 'assignee') && if (
issue.findAssignee(listTo.assignee)) { listTo.type === 'assignee' &&
listFrom.type === 'assignee' &&
issue.findAssignee(listTo.assignee)
) {
const targetIssue = listTo.findIssue(issue.id); const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee); targetIssue.removeAssignee(listFrom.assignee);
} else if (listTo.type === 'milestone') { } else if (listTo.type === 'milestone') {
const currentMilestone = issue.milestone; const currentMilestone = issue.milestone;
const currentLists = this.state.lists const currentLists = this.state.lists
.filter(list => (list.type === 'milestone' && list.id !== listTo.id)) .filter(list => list.type === 'milestone' && list.id !== listTo.id)
.filter(list => list.issues.some(listIssue => issue.id === listIssue.id)); .filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
issue.removeMilestone(currentMilestone); issue.removeMilestone(currentMilestone);
issue.addMilestone(listTo.milestone); issue.addMilestone(listTo.milestone);
...@@ -126,7 +129,7 @@ const boardsStore = { ...@@ -126,7 +129,7 @@ const boardsStore = {
} }
if (listTo.type === 'closed' && listFrom.type !== 'backlog') { if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => { issueLists.forEach(list => {
list.removeIssue(issue); list.removeIssue(issue);
}); });
issue.removeLabels(listLabels); issue.removeLabels(listLabels);
...@@ -144,26 +147,28 @@ const boardsStore = { ...@@ -144,26 +147,28 @@ const boardsStore = {
return ( return (
(listTo.type !== 'label' && listFrom.type === 'assignee') || (listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') || (listTo.type !== 'assignee' && listFrom.type === 'label') ||
(listFrom.type === 'backlog') listFrom.type === 'backlog'
); );
}, },
moveIssueInList (list, issue, oldIndex, newIndex, idArray) { moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null; const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null; const afterId = parseInt(idArray[newIndex + 1], 10) || null;
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
}, },
findList (key, val, type = 'label') { findList(key, val, type = 'label') {
const filteredList = this.state.lists.filter((list) => { const filteredList = this.state.lists.filter(list => {
const byType = type ? (list.type === type) || (list.type === 'assignee') || (list.type === 'milestone') : true; const byType = type
? list.type === type || list.type === 'assignee' || list.type === 'milestone'
: true;
return list[key] === val && byType; return list[key] === val && byType;
}); });
return filteredList[0]; return filteredList[0];
}, },
updateFiltersUrl () { updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`); window.history.pushState(null, null, `?${this.filter.path}`);
} },
}; };
// hacks added in order to allow milestone_select to function properly // hacks added in order to allow milestone_select to function properly
......
...@@ -40,7 +40,7 @@ class ModalStore { ...@@ -40,7 +40,7 @@ class ModalStore {
toggleAll() { toggleAll() {
const select = this.selectedCount() !== this.store.issues.length; const select = this.selectedCount() !== this.store.issues.length;
this.store.issues.forEach((issue) => { this.store.issues.forEach(issue => {
const issueUpdate = issue; const issueUpdate = issue;
if (issueUpdate.selected !== select) { if (issueUpdate.selected !== select) {
...@@ -69,13 +69,14 @@ class ModalStore { ...@@ -69,13 +69,14 @@ class ModalStore {
removeSelectedIssue(issue, forcePurge = false) { removeSelectedIssue(issue, forcePurge = false) {
if (this.store.activeTab === 'all' || forcePurge) { if (this.store.activeTab === 'all' || forcePurge) {
this.store.selectedIssues = this.store.selectedIssues this.store.selectedIssues = this.store.selectedIssues.filter(
.filter(fIssue => fIssue.id !== issue.id); fIssue => fIssue.id !== issue.id,
);
} }
} }
purgeUnselectedIssues() { purgeUnselectedIssues() {
this.store.selectedIssues.forEach((issue) => { this.store.selectedIssues.forEach(issue => {
if (!issue.selected) { if (!issue.selected) {
this.removeSelectedIssue(issue, true); this.removeSelectedIssue(issue, true);
} }
...@@ -87,8 +88,7 @@ class ModalStore { ...@@ -87,8 +88,7 @@ class ModalStore {
} }
findSelectedIssue(issue) { findSelectedIssue(issue) {
return this.store.selectedIssues return this.store.selectedIssues.filter(filteredIssue => filteredIssue.id === issue.id)[0];
.filter(filteredIssue => filteredIssue.id === issue.id)[0];
} }
} }
......
import Vue from 'vue'; import Vue from 'vue';
import { import { GlProgressBar, GlLoadingIcon, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
GlProgressBar,
GlLoadingIcon,
GlTooltipDirective,
} from '@gitlab-org/gitlab-ui';
Vue.component('gl-progress-bar', GlProgressBar); Vue.component('gl-progress-bar', GlProgressBar);
Vue.component('gl-loading-icon', GlLoadingIcon); Vue.component('gl-loading-icon', GlLoadingIcon);
......
...@@ -18,8 +18,8 @@ export default { ...@@ -18,8 +18,8 @@ export default {
}, },
data() { data() {
const treeListStored = localStorage.getItem(treeListStorageKey); const treeListStored = localStorage.getItem(treeListStorageKey);
const renderTreeList = treeListStored !== null ? const renderTreeList =
convertPermissionToBoolean(treeListStored) : true; treeListStored !== null ? convertPermissionToBoolean(treeListStored) : true;
return { return {
search: '', search: '',
......
<script> <script>
import Flash from '../../flash'; import Flash from '../../flash';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import emptyState from './empty_state.vue'; import emptyState from './empty_state.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin'; import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from './stop_environment_modal.vue'; import StopEnvironmentModal from './stop_environment_modal.vue';
export default { export default {
components: { components: {
emptyState, emptyState,
StopEnvironmentModal, StopEnvironmentModal,
}, },
mixins: [ mixins: [CIPaginationMixin, environmentsMixin],
CIPaginationMixin,
environmentsMixin,
],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
required: true, required: true,
},
canCreateEnvironment: {
type: Boolean,
required: true,
},
canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
cssContainerClass: {
type: String,
required: true,
},
newEnvironmentPath: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
}, },
canCreateEnvironment: {
created() { type: Boolean,
eventHub.$on('toggleFolder', this.toggleFolder); required: true,
}, },
canCreateDeployment: {
beforeDestroy() { type: Boolean,
eventHub.$off('toggleFolder'); required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
cssContainerClass: {
type: String,
required: true,
},
newEnvironmentPath: {
type: String,
required: true,
}, },
helpPagePath: {
type: String,
required: true,
},
},
created() {
eventHub.$on('toggleFolder', this.toggleFolder);
},
methods: { beforeDestroy() {
toggleFolder(folder) { eventHub.$off('toggleFolder');
this.store.toggleFolder(folder); },
if (!folder.isOpen) { methods: {
this.fetchChildEnvironments(folder, true); toggleFolder(folder) {
} this.store.toggleFolder(folder);
},
fetchChildEnvironments(folder, showLoader = false) { if (!folder.isOpen) {
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); this.fetchChildEnvironments(folder, true);
}
},
this.service.getFolderContent(folder.folder_path) fetchChildEnvironments(folder, showLoader = false) {
.then(response => this.store.setfolderContent(folder, response.data.environments)) this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
.catch(() => { this.service
Flash(s__('Environments|An error occurred while fetching the environments.')); .getFolderContent(folder.folder_path)
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false); .then(response => this.store.setfolderContent(folder, response.data.environments))
}); .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
}, .catch(() => {
Flash(s__('Environments|An error occurred while fetching the environments.'));
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
});
},
successCallback(resp) { successCallback(resp) {
this.saveData(resp); this.saveData(resp);
// We need to verify if any folder is open to also update it // We need to verify if any folder is open to also update it
const openFolders = this.store.getOpenFolders(); const openFolders = this.store.getOpenFolders();
if (openFolders.length) { if (openFolders.length) {
openFolders.forEach(folder => this.fetchChildEnvironments(folder)); openFolders.forEach(folder => this.fetchChildEnvironments(folder));
} }
},
}, },
}; },
};
</script> </script>
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">
......
...@@ -34,14 +34,14 @@ export default class EnvironmentsStore { ...@@ -34,14 +34,14 @@ export default class EnvironmentsStore {
* @returns {Array} * @returns {Array}
*/ */
storeEnvironments(environments = []) { storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => { const filteredEnvironments = environments.map(env => {
const oldEnvironmentState = this.state.environments const oldEnvironmentState =
.find((element) => { this.state.environments.find(element => {
if (env.latest) { if (env.latest) {
return element.id === env.latest.id; return element.id === env.latest.id;
} }
return element.id === env.id; return element.id === env.id;
}) || {}; }) || {};
let filtered = {}; let filtered = {};
...@@ -101,11 +101,11 @@ export default class EnvironmentsStore { ...@@ -101,11 +101,11 @@ export default class EnvironmentsStore {
} }
/** /**
* Toggles folder open property for the given folder. * Toggles folder open property for the given folder.
* *
* @param {Object} folder * @param {Object} folder
* @return {Array} * @return {Array}
*/ */
toggleFolder(folder) { toggleFolder(folder) {
return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen); return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen);
} }
...@@ -119,7 +119,7 @@ export default class EnvironmentsStore { ...@@ -119,7 +119,7 @@ export default class EnvironmentsStore {
* @return {Object} * @return {Object}
*/ */
setfolderContent(folder, environments) { setfolderContent(folder, environments) {
const updatedEnvironments = environments.map((env) => { const updatedEnvironments = environments.map(env => {
let updated = env; let updated = env;
if (env.latest) { if (env.latest) {
...@@ -148,7 +148,7 @@ export default class EnvironmentsStore { ...@@ -148,7 +148,7 @@ export default class EnvironmentsStore {
updateEnvironmentProp(environment, prop, newValue) { updateEnvironmentProp(environment, prop, newValue) {
const { environments } = this.state; const { environments } = this.state;
const updatedEnvironments = environments.map((env) => { const updatedEnvironments = environments.map(env => {
const updateEnv = Object.assign({}, env); const updateEnv = Object.assign({}, env);
if (env.id === environment.id) { if (env.id === environment.id) {
updateEnv[prop] = newValue; updateEnv[prop] = newValue;
......
...@@ -39,8 +39,9 @@ export default class DropdownUser extends FilteredSearchDropdown { ...@@ -39,8 +39,9 @@ export default class DropdownUser extends FilteredSearchDropdown {
} }
itemClicked(e) { itemClicked(e) {
super.itemClicked(e, super.itemClicked(e, selected =>
selected => selected.querySelector('.dropdown-light-content').innerText.trim()); selected.querySelector('.dropdown-light-content').innerText.trim(),
);
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
...@@ -68,7 +69,7 @@ export default class DropdownUser extends FilteredSearchDropdown { ...@@ -68,7 +69,7 @@ export default class DropdownUser extends FilteredSearchDropdown {
// Removes the first character if it is a quotation so that we can search // Removes the first character if it is a quotation so that we can search
// with multiple words // with multiple words
if (value[0] === '"' || value[0] === '\'') { if (value[0] === '"' || value[0] === "'") {
value = value.slice(1); value = value.slice(1);
} }
......
...@@ -108,7 +108,7 @@ export default class FilteredSearchDropdownManager { ...@@ -108,7 +108,7 @@ export default class FilteredSearchDropdownManager {
}, },
}; };
supportedTokens.forEach((type) => { supportedTokens.forEach(type => {
if (availableMappings[type]) { if (availableMappings[type]) {
allowedMappings[type] = availableMappings[type]; allowedMappings[type] = availableMappings[type];
} }
...@@ -142,10 +142,7 @@ export default class FilteredSearchDropdownManager { ...@@ -142,10 +142,7 @@ export default class FilteredSearchDropdownManager {
} }
static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) {
const { const { uppercaseTokenName = false, capitalizeTokenValue = false } = options;
uppercaseTokenName = false,
capitalizeTokenValue = false,
} = options;
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, { FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, {
uppercaseTokenName, uppercaseTokenName,
...@@ -164,13 +161,16 @@ export default class FilteredSearchDropdownManager { ...@@ -164,13 +161,16 @@ export default class FilteredSearchDropdownManager {
updateDropdownOffset(key) { updateDropdownOffset(key) {
// Always align dropdown with the input field // Always align dropdown with the input field
let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left; let offset =
this.filteredSearchInput.getBoundingClientRect().left -
this.container.querySelector('.scroll-container').getBoundingClientRect().left;
const maxInputWidth = 240; const maxInputWidth = 240;
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
// Make sure offset never exceeds the input container // Make sure offset never exceeds the input container
const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; const offsetMaxWidth =
this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) { if (offsetMaxWidth < offset) {
offset = offsetMaxWidth; offset = offsetMaxWidth;
} }
...@@ -196,8 +196,7 @@ export default class FilteredSearchDropdownManager { ...@@ -196,8 +196,7 @@ export default class FilteredSearchDropdownManager {
const glArguments = Object.assign({}, defaultArguments, extraArguments); const glArguments = Object.assign({}, defaultArguments, extraArguments);
// Passing glArguments to `new glClass(<arguments>)` // Passing glArguments to `new glClass(<arguments>)`
mappingKey.reference = mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
} }
if (firstLoad) { if (firstLoad) {
...@@ -224,8 +223,8 @@ export default class FilteredSearchDropdownManager { ...@@ -224,8 +223,8 @@ export default class FilteredSearchDropdownManager {
} }
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key const shouldOpenFilterDropdown =
&& this.mapping[match.key]; match && this.currentDropdown !== match.key && this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
...@@ -236,8 +235,10 @@ export default class FilteredSearchDropdownManager { ...@@ -236,8 +235,10 @@ export default class FilteredSearchDropdownManager {
setDropdown() { setDropdown() {
const query = DropdownUtils.getSearchQuery(true); const query = DropdownUtils.getSearchQuery(true);
const { lastToken, searchToken } = const { lastToken, searchToken } = this.tokenizer.processTokens(
this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys()); query,
this.filteredSearchTokenKeys.getKeys(),
);
if (this.currentDropdown) { if (this.currentDropdown) {
this.updateCurrentDropdownOffset(); this.updateCurrentDropdownOffset();
......
import _ from 'underscore'; import _ from 'underscore';
import { import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
getParameterByName,
getUrlParamsArray,
} from '~/lib/utils/common_utils';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { visitUrl } from '../lib/utils/url_utility'; import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash'; import Flash from '../flash';
...@@ -48,24 +45,28 @@ export default class FilteredSearchManager { ...@@ -48,24 +45,28 @@ export default class FilteredSearchManager {
isLocalStorageAvailable: RecentSearchesService.isAvailable(), isLocalStorageAvailable: RecentSearchesService.isAvailable(),
allowedKeys: this.filteredSearchTokenKeys.getKeys(), allowedKeys: this.filteredSearchTokenKeys.getKeys(),
}); });
this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); this.searchHistoryDropdownElement = document.querySelector(
const fullPath = this.searchHistoryDropdownElement ? '.js-filtered-search-history-dropdown',
this.searchHistoryDropdownElement.dataset.fullPath : 'project'; );
const fullPath = this.searchHistoryDropdownElement
? this.searchHistoryDropdownElement.dataset.fullPath
: 'project';
const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`; const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey); this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
} }
setup() { setup() {
// Fetch recent searches from localStorage // Fetch recent searches from localStorage
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() this.fetchingRecentSearchesPromise = this.recentSearchesService
.catch((error) => { .fetch()
.catch(error => {
if (error.name === 'RecentSearchesServiceError') return undefined; if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Flash('An error occurred while parsing recent searches'); new Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array // Gracefully fail to empty array
return []; return [];
}) })
.then((searches) => { .then(searches => {
if (!searches) { if (!searches) {
return; return;
} }
...@@ -120,7 +121,7 @@ export default class FilteredSearchManager { ...@@ -120,7 +121,7 @@ export default class FilteredSearchManager {
if (this.stateFilters) { if (this.stateFilters) {
this.searchStateWrapper = this.searchState.bind(this); this.searchStateWrapper = this.searchState.bind(this);
this.applyToStateFilters((filterEl) => { this.applyToStateFilters(filterEl => {
filterEl.addEventListener('click', this.searchStateWrapper); filterEl.addEventListener('click', this.searchStateWrapper);
}); });
} }
...@@ -128,14 +129,14 @@ export default class FilteredSearchManager { ...@@ -128,14 +129,14 @@ export default class FilteredSearchManager {
unbindStateEvents() { unbindStateEvents() {
if (this.stateFilters) { if (this.stateFilters) {
this.applyToStateFilters((filterEl) => { this.applyToStateFilters(filterEl => {
filterEl.removeEventListener('click', this.searchStateWrapper); filterEl.removeEventListener('click', this.searchStateWrapper);
}); });
} }
} }
applyToStateFilters(callback) { applyToStateFilters(callback) {
this.stateFilters.querySelectorAll('a[data-state]').forEach((filterEl) => { this.stateFilters.querySelectorAll('a[data-state]').forEach(filterEl => {
if (this.states.indexOf(filterEl.dataset.state) > -1) { if (this.states.indexOf(filterEl.dataset.state) > -1) {
callback(filterEl); callback(filterEl);
} }
...@@ -207,7 +208,7 @@ export default class FilteredSearchManager { ...@@ -207,7 +208,7 @@ export default class FilteredSearchManager {
let backspaceCount = 0; let backspaceCount = 0;
// closure for keeping track of the number of backspace keystrokes // closure for keeping track of the number of backspace keystrokes
return (e) => { return e => {
// 8 = Backspace Key // 8 = Backspace Key
// 46 = Delete Key // 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
...@@ -274,8 +275,12 @@ export default class FilteredSearchManager { ...@@ -274,8 +275,12 @@ export default class FilteredSearchManager {
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null; const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null; const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown && if (
!isElementInStaticFilterDropdown && inputContainer) { !isElementInFilteredSearch &&
!isElementInDynamicFilterDropdown &&
!isElementInStaticFilterDropdown &&
inputContainer
) {
inputContainer.classList.remove('focus'); inputContainer.classList.remove('focus');
} }
} }
...@@ -368,7 +373,7 @@ export default class FilteredSearchManager { ...@@ -368,7 +373,7 @@ export default class FilteredSearchManager {
const removeElements = []; const removeElements = [];
[].forEach.call(this.tokensContainer.children, (t) => { [].forEach.call(this.tokensContainer.children, t => {
let canClearToken = t.classList.contains('js-visual-token'); let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) { if (canClearToken) {
...@@ -381,7 +386,7 @@ export default class FilteredSearchManager { ...@@ -381,7 +386,7 @@ export default class FilteredSearchManager {
} }
}); });
removeElements.forEach((el) => { removeElements.forEach(el => {
el.parentElement.removeChild(el); el.parentElement.removeChild(el);
}); });
...@@ -397,13 +402,14 @@ export default class FilteredSearchManager { ...@@ -397,13 +402,14 @@ export default class FilteredSearchManager {
handleInputVisualToken() { handleInputVisualToken() {
const input = this.filteredSearchInput; const input = this.filteredSearchInput;
const { tokens, searchToken } const { tokens, searchToken } = this.tokenizer.processTokens(
= this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys()); input.value,
const { isLastVisualTokenValid } this.filteredSearchTokenKeys.getKeys(),
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); );
const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (isLastVisualTokenValid) { if (isLastVisualTokenValid) {
tokens.forEach((t) => { tokens.forEach(t => {
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, { FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, {
uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key), uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key),
...@@ -453,15 +459,17 @@ export default class FilteredSearchManager { ...@@ -453,15 +459,17 @@ export default class FilteredSearchManager {
saveCurrentSearchQuery() { saveCurrentSearchQuery() {
// Don't save before we have fetched the already saved searches // Don't save before we have fetched the already saved searches
this.fetchingRecentSearchesPromise.then(() => { this.fetchingRecentSearchesPromise
const searchQuery = DropdownUtils.getSearchQuery(); .then(() => {
if (searchQuery.length > 0) { const searchQuery = DropdownUtils.getSearchQuery();
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery); if (searchQuery.length > 0) {
this.recentSearchesService.save(resultantSearches); const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
} this.recentSearchesService.save(resultantSearches);
}).catch(() => { }
// https://gitlab.com/gitlab-org/gitlab-ce/issues/30821 })
}); .catch(() => {
// https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
});
} }
// allows for modifying params array when a param can't be included in the URL (e.g. Service Desk) // allows for modifying params array when a param can't be included in the URL (e.g. Service Desk)
...@@ -475,7 +483,7 @@ export default class FilteredSearchManager { ...@@ -475,7 +483,7 @@ export default class FilteredSearchManager {
const usernameParams = this.getUsernameParams(); const usernameParams = this.getUsernameParams();
let hasFilteredSearch = false; let hasFilteredSearch = false;
params.forEach((p) => { params.forEach(p => {
const split = p.split('='); const split = p.split('=');
const keyParam = decodeURIComponent(split[0]); const keyParam = decodeURIComponent(split[0]);
const value = split[1]; const value = split[1];
...@@ -486,11 +494,9 @@ export default class FilteredSearchManager { ...@@ -486,11 +494,9 @@ export default class FilteredSearchManager {
if (condition) { if (condition) {
hasFilteredSearch = true; hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(condition.tokenKey); const canEdit = this.canEdit && this.canEdit(condition.tokenKey);
FilteredSearchVisualTokens.addFilterVisualToken( FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value, {
condition.tokenKey, canEdit,
condition.value, });
{ canEdit },
);
} else { } else {
// Sanitize value since URL converts spaces into + // Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded + // Replace before decode so that we know what was originally + versus the encoded +
...@@ -510,7 +516,7 @@ export default class FilteredSearchManager { ...@@ -510,7 +516,7 @@ export default class FilteredSearchManager {
if (sanitizedValue.indexOf(' ') !== -1) { if (sanitizedValue.indexOf(' ') !== -1) {
// Prefer ", but use ' if required // Prefer ", but use ' if required
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : "'";
} }
hasFilteredSearch = true; hasFilteredSearch = true;
...@@ -531,7 +537,9 @@ export default class FilteredSearchManager { ...@@ -531,7 +537,9 @@ export default class FilteredSearchManager {
hasFilteredSearch = true; hasFilteredSearch = true;
const tokenName = 'assignee'; const tokenName = 'assignee';
const canEdit = this.canEdit && this.canEdit(tokenName); const canEdit = this.canEdit && this.canEdit(tokenName);
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit }); FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, {
canEdit,
});
} }
} else if (!match && keyParam === 'author_id') { } else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10); const id = parseInt(value, 10);
...@@ -539,7 +547,9 @@ export default class FilteredSearchManager { ...@@ -539,7 +547,9 @@ export default class FilteredSearchManager {
hasFilteredSearch = true; hasFilteredSearch = true;
const tokenName = 'author'; const tokenName = 'author';
const canEdit = this.canEdit && this.canEdit(tokenName); const canEdit = this.canEdit && this.canEdit(tokenName);
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit }); FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, {
canEdit,
});
} }
} else if (!match && keyParam === 'search') { } else if (!match && keyParam === 'search') {
hasFilteredSearch = true; hasFilteredSearch = true;
...@@ -580,9 +590,11 @@ export default class FilteredSearchManager { ...@@ -580,9 +590,11 @@ export default class FilteredSearchManager {
const currentState = state || getParameterByName('state') || 'opened'; const currentState = state || getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`); paths.push(`state=${currentState}`);
tokens.forEach((token) => { tokens.forEach(token => {
const condition = this.filteredSearchTokenKeys const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue(
.searchByConditionKeyValue(token.key, token.value.toLowerCase()); token.key,
token.value.toLowerCase(),
);
const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const { param } = tokenConfig; const { param } = tokenConfig;
...@@ -601,8 +613,10 @@ export default class FilteredSearchManager { ...@@ -601,8 +613,10 @@ export default class FilteredSearchManager {
tokenValue = tokenValue.toLowerCase(); tokenValue = tokenValue.toLowerCase();
} }
if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || if (
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { (tokenValue[0] === "'" && tokenValue[tokenValue.length - 1] === "'") ||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')
) {
tokenValue = tokenValue.slice(1, tokenValue.length - 1); tokenValue = tokenValue.slice(1, tokenValue.length - 1);
} }
...@@ -613,7 +627,10 @@ export default class FilteredSearchManager { ...@@ -613,7 +627,10 @@ export default class FilteredSearchManager {
}); });
if (searchToken) { if (searchToken) {
const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+'); const sanitized = searchToken
.split(' ')
.map(t => encodeURIComponent(t))
.join('+');
paths.push(`search=${sanitized}`); paths.push(`search=${sanitized}`);
} }
...@@ -630,7 +647,7 @@ export default class FilteredSearchManager { ...@@ -630,7 +647,7 @@ export default class FilteredSearchManager {
const usernamesById = {}; const usernamesById = {};
try { try {
const attribute = this.filteredSearchInput.getAttribute('data-username-params'); const attribute = this.filteredSearchInput.getAttribute('data-username-params');
JSON.parse(attribute).forEach((user) => { JSON.parse(attribute).forEach(user => {
usernamesById[user.id] = user.username; usernamesById[user.id] = user.username;
}); });
} catch (e) { } catch (e) {
......
...@@ -40,7 +40,9 @@ const createFlashEl = (message, type, isFixedLayout = false) => ` ...@@ -40,7 +40,9 @@ const createFlashEl = (message, type, isFixedLayout = false) => `
class="flash-${type}" class="flash-${type}"
> >
<div <div
class="flash-text ${isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''}" class="flash-text ${
isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''
}"
> >
${_.escape(message)} ${_.escape(message)}
</div> </div>
...@@ -78,7 +80,9 @@ const createFlash = function createFlash( ...@@ -78,7 +80,9 @@ const createFlash = function createFlash(
if (!flashContainer) return null; if (!flashContainer) return null;
const isFixedLayout = navigation ? navigation.parentNode.classList.contains('container-limited') : true; const isFixedLayout = navigation
? navigation.parentNode.classList.contains('container-limited')
: true;
flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout); flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout);
......
...@@ -94,7 +94,7 @@ class GfmAutoComplete { ...@@ -94,7 +94,7 @@ class GfmAutoComplete {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(commands) { beforeSave(commands) {
if (GfmAutoComplete.isLoading(commands)) return commands; if (GfmAutoComplete.isLoading(commands)) return commands;
return $.map(commands, (c) => { return $.map(commands, c => {
let search = c.name; let search = c.name;
if (c.aliases.length > 0) { if (c.aliases.length > 0) {
search = `${search} ${c.aliases.join(' ')}`; search = `${search} ${c.aliases.join(' ')}`;
...@@ -167,7 +167,7 @@ class GfmAutoComplete { ...@@ -167,7 +167,7 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(members) { beforeSave(members) {
return $.map(members, (m) => { return $.map(members, m => {
let title = ''; let title = '';
if (m.username == null) { if (m.username == null) {
return m; return m;
...@@ -178,7 +178,9 @@ class GfmAutoComplete { ...@@ -178,7 +178,9 @@ class GfmAutoComplete {
} }
const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`; const imgAvatar = `<img src="${m.avatar_url}" alt="${
m.username
}" class="avatar avatar-inline center s26"/>`;
const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`; const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
return { return {
...@@ -211,7 +213,7 @@ class GfmAutoComplete { ...@@ -211,7 +213,7 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(issues) { beforeSave(issues) {
return $.map(issues, (i) => { return $.map(issues, i => {
if (i.title == null) { if (i.title == null) {
return i; return i;
} }
...@@ -244,7 +246,7 @@ class GfmAutoComplete { ...@@ -244,7 +246,7 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(milestones) { beforeSave(milestones) {
return $.map(milestones, (m) => { return $.map(milestones, m => {
if (m.title == null) { if (m.title == null) {
return m; return m;
} }
...@@ -277,7 +279,7 @@ class GfmAutoComplete { ...@@ -277,7 +279,7 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(merges) { beforeSave(merges) {
return $.map(merges, (m) => { return $.map(merges, m => {
if (m.title == null) { if (m.title == null) {
return m; return m;
} }
...@@ -324,13 +326,20 @@ class GfmAutoComplete { ...@@ -324,13 +326,20 @@ class GfmAutoComplete {
}, },
matcher(flag, subtext) { matcher(flag, subtext) {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext); const subtextNodes = subtext
.split(/\n+/g)
.pop()
.split(GfmAutoComplete.regexSubtext);
// Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands. // Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
command = subtextNodes.find((node) => { command = subtextNodes.find(node => {
if (node === LABEL_COMMAND.LABEL || if (
node === LABEL_COMMAND.RELABEL || node === LABEL_COMMAND.LABEL ||
node === LABEL_COMMAND.UNLABEL) { return node; } node === LABEL_COMMAND.RELABEL ||
node === LABEL_COMMAND.UNLABEL
) {
return node;
}
return null; return null;
}); });
...@@ -380,7 +389,7 @@ class GfmAutoComplete { ...@@ -380,7 +389,7 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(snippets) { beforeSave(snippets) {
return $.map(snippets, (m) => { return $.map(snippets, m => {
if (m.title == null) { if (m.title == null) {
return m; return m;
} }
...@@ -458,13 +467,17 @@ class GfmAutoComplete { ...@@ -458,13 +467,17 @@ class GfmAutoComplete {
this.loadData($input, at, validEmojiNames); this.loadData($input, at, validEmojiNames);
GfmAutoComplete.glEmojiTag = glEmojiTag; GfmAutoComplete.glEmojiTag = glEmojiTag;
}) })
.catch(() => { this.isLoadingData[at] = false; }); .catch(() => {
this.isLoadingData[at] = false;
});
} else if (dataSource) { } else if (dataSource) {
AjaxCache.retrieve(dataSource, true) AjaxCache.retrieve(dataSource, true)
.then((data) => { .then(data => {
this.loadData($input, at, data); this.loadData($input, at, data);
}) })
.catch(() => { this.isLoadingData[at] = false; }); .catch(() => {
this.isLoadingData[at] = false;
});
} else { } else {
this.isLoadingData[at] = false; this.isLoadingData[at] = false;
} }
...@@ -497,15 +510,16 @@ class GfmAutoComplete { ...@@ -497,15 +510,16 @@ class GfmAutoComplete {
} }
const loadingState = GfmAutoComplete.defaultLoadingData[0]; const loadingState = GfmAutoComplete.defaultLoadingData[0];
return dataToInspect && return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState);
(dataToInspect === loadingState || dataToInspect.name === loadingState);
} }
static defaultMatcher(flag, subtext, controllers) { static defaultMatcher(flag, subtext, controllers) {
// The below is taken from At.js source // The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character // Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js // https://github.com/ichord/At.js
const atSymbolsWithBar = Object.keys(controllers).join('|').replace(/[$]/, '\\$&'); const atSymbolsWithBar = Object.keys(controllers)
.join('|')
.replace(/[$]/, '\\$&');
const atSymbolsWithoutBar = Object.keys(controllers).join(''); const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop(); const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
...@@ -513,7 +527,10 @@ class GfmAutoComplete { ...@@ -513,7 +527,10 @@ class GfmAutoComplete {
const accentAChar = decodeURI('%C3%80'); const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF'); const accentYChar = decodeURI('%C3%BF');
const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); const regexp = new RegExp(
`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`,
'gi',
);
return regexp.exec(targetSubtext); return regexp.exec(targetSubtext);
} }
...@@ -553,7 +570,8 @@ GfmAutoComplete.Members = { ...@@ -553,7 +570,8 @@ GfmAutoComplete.Members = {
}; };
GfmAutoComplete.Labels = { GfmAutoComplete.Labels = {
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', template:
'<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
}; };
// Issues, MergeRequests and Snippets // Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = { GfmAutoComplete.Issues = {
...@@ -567,7 +585,8 @@ GfmAutoComplete.Milestones = { ...@@ -567,7 +585,8 @@ GfmAutoComplete.Milestones = {
template: '<li>${title}</li>', template: '<li>${title}</li>',
}; };
GfmAutoComplete.Loading = { GfmAutoComplete.Loading = {
template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>', template:
'<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>',
}; };
export default GfmAutoComplete; export default GfmAutoComplete;
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { mapGetters, mapState, mapActions } from 'vuex'; import { mapGetters, mapState, mapActions } from 'vuex';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import bp from '~/breakpoints'; import bp from '~/breakpoints';
import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue'; import Callout from '~/vue_shared/components/callout.vue';
import createStore from '../store'; import createStore from '../store';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue'; import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue'; import ErasedBlock from './erased_block.vue';
import Log from './job_log.vue'; import Log from './job_log.vue';
import LogTopBar from './job_log_controllers.vue'; import LogTopBar from './job_log_controllers.vue';
import StuckBlock from './stuck_block.vue'; import StuckBlock from './stuck_block.vue';
import Sidebar from './sidebar.vue'; import Sidebar from './sidebar.vue';
export default { export default {
name: 'JobPageApp', name: 'JobPageApp',
store: createStore(), store: createStore(),
components: { components: {
CiHeader, CiHeader,
Callout, Callout,
EmptyState, EmptyState,
EnvironmentsBlock, EnvironmentsBlock,
ErasedBlock, ErasedBlock,
Log, Log,
LogTopBar, LogTopBar,
StuckBlock, StuckBlock,
Sidebar, Sidebar,
},
props: {
runnerSettingsUrl: {
type: String,
required: false,
default: null,
}, },
props: { runnerHelpUrl: {
runnerSettingsUrl: { type: String,
type: String, required: false,
required: false, default: null,
default: null,
},
runnerHelpUrl: {
type: String,
required: false,
default: null,
},
endpoint: {
type: String,
required: true,
},
terminalPath: {
type: String,
required: false,
default: null,
},
pagePath: {
type: String,
required: true,
},
logState: {
type: String,
required: true,
},
}, },
computed: { endpoint: {
...mapState([ type: String,
'isLoading', required: true,
'job', },
'isSidebarOpen', terminalPath: {
'trace', type: String,
'isTraceComplete', required: false,
'traceSize', default: null,
'isTraceSizeVisible', },
'isScrollBottomDisabled', pagePath: {
'isScrollTopDisabled', type: String,
'isScrolledToBottomBeforeReceivingTrace', required: true,
'hasError', },
]), logState: {
...mapGetters([ type: String,
'headerActions', required: true,
'headerTime', },
'shouldRenderCalloutMessage', },
'shouldRenderTriggeredLabel', computed: {
'hasEnvironment', ...mapState([
'hasTrace', 'isLoading',
'emptyStateIllustration', 'job',
'isScrollingDown', 'isSidebarOpen',
'emptyStateAction', 'trace',
'hasRunnersForProject', 'isTraceComplete',
]), 'traceSize',
'isTraceSizeVisible',
'isScrollBottomDisabled',
'isScrollTopDisabled',
'isScrolledToBottomBeforeReceivingTrace',
'hasError',
]),
...mapGetters([
'headerActions',
'headerTime',
'shouldRenderCalloutMessage',
'shouldRenderTriggeredLabel',
'hasEnvironment',
'hasTrace',
'emptyStateIllustration',
'isScrollingDown',
'emptyStateAction',
'hasRunnersForProject',
]),
shouldRenderContent() { shouldRenderContent() {
return !this.isLoading && !this.hasError; return !this.isLoading && !this.hasError;
}
}, },
watch: { },
// Once the job log is loaded, watch: {
// fetch the stages for the dropdown on the sidebar // Once the job log is loaded,
job(newVal, oldVal) { // fetch the stages for the dropdown on the sidebar
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { job(newVal, oldVal) {
this.fetchStages(); if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
} this.fetchStages();
}, }
}, },
created() { },
this.throttled = _.throttle(this.toggleScrollButtons, 100); created() {
this.throttled = _.throttle(this.toggleScrollButtons, 100);
this.setJobEndpoint(this.endpoint); this.setJobEndpoint(this.endpoint);
this.setTraceOptions({ this.setTraceOptions({
logState: this.logState, logState: this.logState,
pagePath: this.pagePath, pagePath: this.pagePath,
}); });
this.fetchJob(); this.fetchJob();
this.fetchTrace(); this.fetchTrace();
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
window.addEventListener('scroll', this.updateScroll); window.addEventListener('scroll', this.updateScroll);
}, },
mounted() { mounted() {
this.updateSidebar();
},
destroyed() {
window.removeEventListener('resize', this.onResize);
window.removeEventListener('scroll', this.updateScroll);
},
methods: {
...mapActions([
'setJobEndpoint',
'setTraceOptions',
'fetchJob',
'fetchStages',
'hideSidebar',
'showSidebar',
'toggleSidebar',
'fetchTrace',
'scrollBottom',
'scrollTop',
'toggleScrollButtons',
'toggleScrollAnimation',
]),
onResize() {
this.updateSidebar(); this.updateSidebar();
this.updateScroll();
}, },
updateSidebar() {
destroyed() { if (bp.getBreakpointSize() === 'xs') {
window.removeEventListener('resize', this.onResize); this.hideSidebar();
window.removeEventListener('scroll', this.updateScroll); } else if (!this.isSidebarOpen) {
this.showSidebar();
}
}, },
updateScroll() {
if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
} else if (this.isScrollingDown) {
this.toggleScrollAnimation(true);
}
methods: { this.throttled();
...mapActions([
'setJobEndpoint',
'setTraceOptions',
'fetchJob',
'fetchStages',
'hideSidebar',
'showSidebar',
'toggleSidebar',
'fetchTrace',
'scrollBottom',
'scrollTop',
'toggleScrollButtons',
'toggleScrollAnimation',
]),
onResize() {
this.updateSidebar();
this.updateScroll();
},
updateSidebar() {
if (bp.getBreakpointSize() === 'xs') {
this.hideSidebar();
} else if (!this.isSidebarOpen) {
this.showSidebar();
}
},
updateScroll() {
if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
} else if (this.isScrollingDown) {
this.toggleScrollAnimation(true);
}
this.throttled();
},
}, },
}; },
};
</script> </script>
<template> <template>
<div> <div>
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
export default { export default {
name: 'JobLog', name: 'JobLog',
props: { props: {
trace: { trace: {
type: String, type: String,
required: true, required: true,
},
isComplete: {
type: Boolean,
required: true,
},
}, },
computed: { isComplete: {
...mapState(['isScrolledToBottomBeforeReceivingTrace']), type: Boolean,
required: true,
}, },
updated() { },
this.$nextTick(() => this.handleScrollDown()); computed: {
...mapState(['isScrolledToBottomBeforeReceivingTrace']),
},
updated() {
this.$nextTick(() => this.handleScrollDown());
},
mounted() {
this.$nextTick(() => this.handleScrollDown());
},
methods: {
...mapActions(['scrollBottom']),
/**
* The job log is sent in HTML, which means we need to use `v-html` to render it
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
* in this case because it runs before `v-html` has finished running, since there's no
* Vue binding.
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
*/
handleScrollDown() {
if (this.isScrolledToBottomBeforeReceivingTrace) {
setTimeout(() => {
this.scrollBottom();
}, 0);
}
}, },
mounted() { },
this.$nextTick(() => this.handleScrollDown()); };
},
methods: {
...mapActions(['scrollBottom']),
/**
* The job log is sent in HTML, which means we need to use `v-html` to render it
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
* in this case because it runs before `v-html` has finished running, since there's no
* Vue binding.
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
*/
handleScrollDown() {
if (this.isScrolledToBottomBeforeReceivingTrace) {
setTimeout(() => {
this.scrollBottom();
}, 0);
}
},
},
};
</script> </script>
<template> <template>
<pre class="js-build-trace build-trace qa-build-trace"> <pre class="js-build-trace build-trace qa-build-trace">
......
...@@ -23,4 +23,3 @@ export default () => { ...@@ -23,4 +23,3 @@ export default () => {
}, },
}); });
}; };
...@@ -35,16 +35,19 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); ...@@ -35,16 +35,19 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* Used to check if it should render the job log or the empty state * Used to check if it should render the job log or the empty state
* @returns {Boolean} * @returns {Boolean}
*/ */
export const hasTrace = state => state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running'); export const hasTrace = state =>
state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running');
export const emptyStateIllustration = state => export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {}; (state.job && state.job.status && state.job.status.illustration) || {};
export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {}; export const emptyStateAction = state =>
(state.job && state.job.status && state.job.status.action) || {};
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
export const hasRunnersForProject = state => state.job.runners.available && !state.job.runners.online; export const hasRunnersForProject = state =>
state.job.runners.available && !state.job.runners.online;
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -25,7 +25,35 @@ export default class LabelsSelect { ...@@ -25,7 +25,35 @@ export default class LabelsSelect {
} }
$els.each(function(i, dropdown) { $els.each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; var $block,
$colorPreview,
$dropdown,
$form,
$loading,
$selectbox,
$sidebarCollapsedValue,
$value,
abilityName,
defaultLabel,
enableLabelCreateButton,
issueURLSplit,
issueUpdateURL,
labelUrl,
namespacePath,
projectPath,
saveLabelData,
selectedLabel,
showAny,
showNo,
$sidebarLabelTooltip,
initialSelected,
$toggleText,
fieldName,
useId,
propertyName,
showMenuAbove,
$container,
$dropdownContainer;
$dropdown = $(dropdown); $dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter'); $dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text'); $toggleText = $dropdown.find('.dropdown-toggle-text');
...@@ -34,7 +62,7 @@ export default class LabelsSelect { ...@@ -34,7 +62,7 @@ export default class LabelsSelect {
labelUrl = $dropdown.data('labels'); labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate'); issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected'); selectedLabel = $dropdown.data('selected');
if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) { if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) {
selectedLabel = selectedLabel.split(','); selectedLabel = selectedLabel.split(',');
} }
showNo = $dropdown.data('showNo'); showNo = $dropdown.data('showNo');
...@@ -50,26 +78,37 @@ export default class LabelsSelect { ...@@ -50,26 +78,37 @@ export default class LabelsSelect {
$value = $block.find('.value'); $value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut(); $loading = $block.find('.block-loading').fadeOut();
fieldName = $dropdown.data('fieldName'); fieldName = $dropdown.data('fieldName');
useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown'); useId = $dropdown.is(
'.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown',
);
propertyName = useId ? 'id' : 'title'; propertyName = useId ? 'id' : 'title';
initialSelected = $selectbox initialSelected = $selectbox
.find('input[name="' + $dropdown.data('fieldName') + '"]') .find('input[name="' + $dropdown.data('fieldName') + '"]')
.map(function () { .map(function() {
return this.value; return this.value;
}).get(); })
.get();
const { handleClick } = options; const { handleClick } = options;
$sidebarLabelTooltip.tooltip(); $sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); new CreateLabelDropdown(
$dropdown.closest('.dropdown').find('.dropdown-new-label'),
namespacePath,
projectPath,
);
} }
saveLabelData = function() { saveLabelData = function() {
var data, selected; var data, selected;
selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() { selected = $dropdown
return this.value; .closest('.selectbox')
}).get(); .find("input[name='" + fieldName + "']")
.map(function() {
return this.value;
})
.get();
if (_.isEqual(initialSelected, selected)) return; if (_.isEqual(initialSelected, selected)) return;
initialSelected = selected; initialSelected = selected;
...@@ -82,7 +121,8 @@ export default class LabelsSelect { ...@@ -82,7 +121,8 @@ export default class LabelsSelect {
} }
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
axios.put(issueUpdateURL, data) axios
.put(issueUpdateURL, data)
.then(({ data }) => { .then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels; var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut(); $loading.fadeOut();
...@@ -96,8 +136,7 @@ export default class LabelsSelect { ...@@ -96,8 +136,7 @@ export default class LabelsSelect {
issueUpdateURL, issueUpdateURL,
}); });
labelCount = data.labels.length; labelCount = data.labels.length;
} } else {
else {
template = '<span class="no-value">None</span>'; template = '<span class="no-value">None</span>';
} }
$value.removeAttr('style').html(template); $value.removeAttr('style').html(template);
...@@ -114,17 +153,14 @@ export default class LabelsSelect { ...@@ -114,17 +153,14 @@ export default class LabelsSelect {
} }
labelTooltipTitle = labelTitles.join(', '); labelTooltipTitle = labelTitles.join(', ');
} } else {
else {
labelTooltipTitle = __('Labels'); labelTooltipTitle = __('Labels');
} }
$sidebarLabelTooltip $sidebarLabelTooltip.attr('title', labelTooltipTitle).tooltip('_fixTitle');
.attr('title', labelTooltipTitle)
.tooltip('_fixTitle');
$('.has-tooltip', $value).tooltip({ $('.has-tooltip', $value).tooltip({
container: 'body' container: 'body',
}); });
}) })
.catch(() => flash(__('Error saving label update.'))); .catch(() => flash(__('Error saving label update.')));
...@@ -132,34 +168,38 @@ export default class LabelsSelect { ...@@ -132,34 +168,38 @@ export default class LabelsSelect {
$dropdown.glDropdown({ $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
axios.get(labelUrl) axios
.then((res) => { .get(labelUrl)
let data = _.chain(res.data).groupBy(function(label) { .then(res => {
return label.title; let data = _.chain(res.data)
}).map(function(label) { .groupBy(function(label) {
var color; return label.title;
color = _.map(label, function(dup) { })
return dup.color; .map(function(label) {
}); var color;
return { color = _.map(label, function(dup) {
id: label[0].id, return dup.color;
title: label[0].title, });
color: color, return {
duplicate: color.length > 1 id: label[0].id,
}; title: label[0].title,
}).value(); color: color,
duplicate: color.length > 1,
};
})
.value();
if ($dropdown.hasClass('js-extra-options')) { if ($dropdown.hasClass('js-extra-options')) {
var extraData = []; var extraData = [];
if (showNo) { if (showNo) {
extraData.unshift({ extraData.unshift({
id: 0, id: 0,
title: 'No Label' title: 'No Label',
}); });
} }
if (showAny) { if (showAny) {
extraData.unshift({ extraData.unshift({
isAny: true, isAny: true,
title: 'Any Label' title: 'Any Label',
}); });
} }
if (extraData.length) { if (extraData.length) {
...@@ -176,11 +216,22 @@ export default class LabelsSelect { ...@@ -176,11 +216,22 @@ export default class LabelsSelect {
.catch(() => flash(__('Error fetching labels.'))); .catch(() => flash(__('Error fetching labels.')));
}, },
renderRow: function(label, instance) { renderRow: function(label, instance) {
var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; var $a,
$li,
color,
colorEl,
indeterminate,
removesAll,
selectedClass,
spacing,
i,
marked,
dropdownName,
dropdownValue;
$li = $('<li>'); $li = $('<li>');
$a = $('<a href="#">'); $a = $('<a href="#">');
selectedClass = []; selectedClass = [];
removesAll = label.id <= 0 || (label.id == null); removesAll = label.id <= 0 || label.id == null;
if ($dropdown.hasClass('js-filter-bulk-update')) { if ($dropdown.hasClass('js-filter-bulk-update')) {
indeterminate = $dropdown.data('indeterminate') || []; indeterminate = $dropdown.data('indeterminate') || [];
marked = $dropdown.data('marked') || []; marked = $dropdown.data('marked') || [];
...@@ -200,9 +251,19 @@ export default class LabelsSelect { ...@@ -200,9 +251,19 @@ export default class LabelsSelect {
} else { } else {
if (this.id(label)) { if (this.id(label)) {
dropdownName = $dropdown.data('fieldName'); dropdownName = $dropdown.data('fieldName');
dropdownValue = this.id(label).toString().replace(/'/g, '\\\''); dropdownValue = this.id(label)
.toString()
if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) { .replace(/'/g, "\\'");
if (
$form.find(
"input[type='hidden'][name='" +
dropdownName +
"'][value='" +
dropdownValue +
"']",
).length
) {
selectedClass.push('is-active'); selectedClass.push('is-active');
} }
} }
...@@ -213,16 +274,14 @@ export default class LabelsSelect { ...@@ -213,16 +274,14 @@ export default class LabelsSelect {
} }
if (label.duplicate) { if (label.duplicate) {
color = DropdownUtils.duplicateLabelColor(label.color); color = DropdownUtils.duplicateLabelColor(label.color);
} } else {
else {
if (label.color != null) { if (label.color != null) {
[color] = label.color; [color] = label.color;
} }
} }
if (color) { if (color) {
colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>"; colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
} } else {
else {
colorEl = ''; colorEl = '';
} }
// We need to identify which items are actually labels // We need to identify which items are actually labels
...@@ -235,7 +294,7 @@ export default class LabelsSelect { ...@@ -235,7 +294,7 @@ export default class LabelsSelect {
return $li.html($a).prop('outerHTML'); return $li.html($a).prop('outerHTML');
}, },
search: { search: {
fields: ['title'] fields: ['title'],
}, },
selectable: true, selectable: true,
filterable: true, filterable: true,
...@@ -255,25 +314,21 @@ export default class LabelsSelect { ...@@ -255,25 +314,21 @@ export default class LabelsSelect {
if (selected && selected.id === 0) { if (selected && selected.id === 0) {
this.selected = []; this.selected = [];
return 'No Label'; return 'No Label';
} } else if (isSelected) {
else if (isSelected) {
this.selected.push(title); this.selected.push(title);
} } else if (!isSelected && title) {
else if (!isSelected && title) {
var index = this.selected.indexOf(title); var index = this.selected.indexOf(title);
this.selected.splice(index, 1); this.selected.splice(index, 1);
} }
if (selectedLabels.length === 1) { if (selectedLabels.length === 1) {
return selectedLabels; return selectedLabels;
} } else if (selectedLabels.length) {
else if (selectedLabels.length) {
return sprintf(__('%{firstLabel} +%{labelCount} more'), { return sprintf(__('%{firstLabel} +%{labelCount} more'), {
firstLabel: selectedLabels[0], firstLabel: selectedLabels[0],
labelCount: selectedLabels.length - 1 labelCount: selectedLabels.length - 1,
}); });
} } else {
else {
return defaultLabel; return defaultLabel;
} }
}, },
...@@ -285,10 +340,9 @@ export default class LabelsSelect { ...@@ -285,10 +340,9 @@ export default class LabelsSelect {
return label.id; return label.id;
} }
if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) { if ($dropdown.hasClass('js-filter-submit') && label.isAny == null) {
return label.title; return label.title;
} } else {
else {
return label.id; return label.id;
} }
}, },
...@@ -310,13 +364,13 @@ export default class LabelsSelect { ...@@ -310,13 +364,13 @@ export default class LabelsSelect {
} }
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']"); selectedLabels = $dropdown
.closest('form')
.find("input:hidden[name='" + $dropdown.data('fieldName') + "']");
Issuable.filterResults($dropdown.closest('form')); Issuable.filterResults($dropdown.closest('form'));
} } else if ($dropdown.hasClass('js-filter-submit')) {
else if ($dropdown.hasClass('js-filter-submit')) {
$dropdown.closest('form').submit(); $dropdown.closest('form').submit();
} } else {
else {
if (!$dropdown.hasClass('js-filter-bulk-update')) { if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData(); saveLabelData();
} }
...@@ -325,7 +379,7 @@ export default class LabelsSelect { ...@@ -325,7 +379,7 @@ export default class LabelsSelect {
}, },
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function (clickEvent) { clicked: function(clickEvent) {
const { $el, e, isMarking } = clickEvent; const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj; const label = clickEvent.selectedObj;
...@@ -339,7 +393,8 @@ export default class LabelsSelect { ...@@ -339,7 +393,8 @@ export default class LabelsSelect {
isMRIndex = page === 'projects:merge_requests:index'; isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown.parent() $dropdown
.parent()
.find('.dropdown-clear-active') .find('.dropdown-clear-active')
.removeClass('is-active'); .removeClass('is-active');
} }
...@@ -367,28 +422,26 @@ export default class LabelsSelect { ...@@ -367,28 +422,26 @@ export default class LabelsSelect {
e.preventDefault(); e.preventDefault();
return; return;
} } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) { if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title; selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} }
} } else if ($dropdown.hasClass('js-filter-submit')) {
else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit(); return $dropdown.closest('form').submit();
} } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if ($el.hasClass('is-active')) { if ($el.hasClass('is-active')) {
boardsStore.detail.issue.labels.push(new ListLabel({ boardsStore.detail.issue.labels.push(
id: label.id, new ListLabel({
title: label.title, id: label.id,
color: label.color[0], title: label.title,
textColor: '#fff' color: label.color[0],
})); textColor: '#fff',
} }),
else { );
} else {
var { labels } = boardsStore.detail.issue; var { labels } = boardsStore.detail.issue;
labels = labels.filter(function (selectedLabel) { labels = labels.filter(function(selectedLabel) {
return selectedLabel.id !== label.id; return selectedLabel.id !== label.id;
}); });
boardsStore.detail.issue.labels = labels; boardsStore.detail.issue.labels = labels;
...@@ -396,19 +449,16 @@ export default class LabelsSelect { ...@@ -396,19 +449,16 @@ export default class LabelsSelect {
$loading.fadeIn(); $loading.fadeIn();
boardsStore.detail.issue.update($dropdown.attr('data-issue-update')) boardsStore.detail.issue
.update($dropdown.attr('data-issue-update'))
.then(fadeOutLoader) .then(fadeOutLoader)
.catch(fadeOutLoader); .catch(fadeOutLoader);
} } else if (handleClick) {
else if (handleClick) {
e.preventDefault(); e.preventDefault();
handleClick(label); handleClick(label);
} } else {
else {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
} else {
}
else {
return saveLabelData(); return saveLabelData();
} }
} }
...@@ -436,15 +486,17 @@ export default class LabelsSelect { ...@@ -436,15 +486,17 @@ export default class LabelsSelect {
// so best approach is to use traditional way of // so best approach is to use traditional way of
// concatenation // concatenation
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays // see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
const tpl = _.template([ const tpl = _.template(
'<% _.each(labels, function(label){ %>', [
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', '<% _.each(labels, function(label){ %>',
'<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
'<%- label.title %>', '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
'</span>', '<%- label.title %>',
'</a>', '</span>',
'<% }); %>', '</a>',
].join('')); '<% }); %>',
].join(''),
);
return tpl(tplData); return tpl(tplData);
} }
......
/* global ace */ /* global ace */
export default function getModeByFileExtension(path) { export default function getModeByFileExtension(path) {
const modelist = ace.require("ace/ext/modelist"); const modelist = ace.require('ace/ext/modelist');
return modelist.getModeForPath(path).mode; return modelist.getModeForPath(path).mode;
}; }
...@@ -7,7 +7,7 @@ import { BYTES_IN_KIB } from './constants'; ...@@ -7,7 +7,7 @@ import { BYTES_IN_KIB } from './constants';
* * * Show 3 digits to the right * * * Show 3 digits to the right
* * For 2 digits to the left of the decimal point and X digits to the right of it * * For 2 digits to the left of the decimal point and X digits to the right of it
* * * Show 2 digits to the right * * * Show 2 digits to the right
*/ */
export function formatRelevantDigits(number) { export function formatRelevantDigits(number) {
let digitsLeft = ''; let digitsLeft = '';
let relevantDigits = 0; let relevantDigits = 0;
......
...@@ -7,8 +7,12 @@ export default class Members { ...@@ -7,8 +7,12 @@ export default class Members {
} }
addListeners() { addListeners() {
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); $('.js-member-update-control')
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); .off('change')
.on('change', this.formSubmit.bind(this));
$('.js-edit-member-form')
.off('ajax:success')
.on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
} }
...@@ -28,7 +32,7 @@ export default class Members { ...@@ -28,7 +32,7 @@ export default class Members {
toggleLabel(selected, $el) { toggleLabel(selected, $el) {
return $el.text(); return $el.text();
}, },
clicked: (options) => { clicked: options => {
this.formSubmit(null, options.$el); this.formSubmit(null, options.$el);
}, },
}); });
......
...@@ -9,7 +9,10 @@ import '~/gl_dropdown'; ...@@ -9,7 +9,10 @@ import '~/gl_dropdown';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store'; import ModalStore from './boards/stores/modal_store';
import boardsStore, { boardStoreIssueSet, boardStoreIssueDelete } from './boards/stores/boards_store'; import boardsStore, {
boardStoreIssueSet,
boardStoreIssueDelete,
} from './boards/stores/boards_store';
export default class MilestoneSelect { export default class MilestoneSelect {
constructor(currentProject, els, options = {}) { constructor(currentProject, els, options = {}) {
......
...@@ -54,7 +54,13 @@ export default { ...@@ -54,7 +54,13 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']), ...mapGetters([
'isNotesFetched',
'discussions',
'getNotesDataByProp',
'discussionCount',
'isLoading',
]),
noteableType() { noteableType() {
return this.noteableData.noteableType; return this.noteableData.noteableType;
}, },
......
import Vue from 'vue'; import Vue from 'vue';
import DiscussionFilter from './components/discussion_filter.vue'; import DiscussionFilter from './components/discussion_filter.vue';
export default (store) => { export default store => {
const discussionFilterEl = document.getElementById('js-vue-discussion-filter'); const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
if (discussionFilterEl) { if (discussionFilterEl) {
const { defaultFilter, notesFilters } = discussionFilterEl.dataset; const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null; const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
const filters = Object.keys(filterValues).map(entry => const filters = Object.keys(filterValues).map(entry => ({
({ title: entry, value: filterValues[entry] })); title: entry,
value: filterValues[entry],
}));
return new Vue({ return new Vue({
el: discussionFilterEl, el: discussionFilterEl,
......
<script> <script>
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue'; import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
components: { components: {
GlModal, GlModal,
},
props: {
milestoneTitle: {
type: String,
required: true,
}, },
props: { url: {
milestoneTitle: { type: String,
type: String, required: true,
required: true,
},
url: {
type: String,
required: true,
},
groupName: {
type: String,
required: true,
},
}, },
computed: { groupName: {
title() { type: String,
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle }); required: true,
}, },
text() { },
return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. computed: {
title() {
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), {
milestoneTitle: this.milestoneTitle,
});
},
text() {
return sprintf(
s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
Existing project milestones with the same title will be merged. Existing project milestones with the same title will be merged.
This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName }); This action cannot be reversed.`),
}, { milestoneTitle: this.milestoneTitle, groupName: this.groupName },
);
}, },
methods: { },
onSubmit() { methods: {
eventHub.$emit('promoteMilestoneModal.requestStarted', this.url); onSubmit() {
return axios.post(this.url, { params: { format: 'json' } }) eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
.then((response) => { return axios
eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true }); .post(this.url, { params: { format: 'json' } })
visitUrl(response.data.url); .then(response => {
}) eventHub.$emit('promoteMilestoneModal.requestFinished', {
.catch((error) => { milestoneUrl: this.url,
eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false }); successful: true,
createFlash(error);
}); });
}, visitUrl(response.data.url);
})
.catch(error => {
eventHub.$emit('promoteMilestoneModal.requestFinished', {
milestoneUrl: this.url,
successful: false,
});
createFlash(error);
});
}, },
}; },
};
</script> </script>
<template> <template>
<gl-modal <gl-modal
...@@ -65,4 +77,3 @@ ...@@ -65,4 +77,3 @@
{{ text }} {{ text }}
</gl-modal> </gl-modal>
</template> </template>
...@@ -64,7 +64,9 @@ export default class Project { ...@@ -64,7 +64,9 @@ export default class Project {
const projectId = $(this).data('project-id'); const projectId = $(this).data('project-id');
const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`; const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`;
Cookies.set(cookieKey, 'false'); Cookies.set(cookieKey, 'false');
$(this).parents('.auto-devops-implicitly-enabled-banner').remove(); $(this)
.parents('.auto-devops-implicitly-enabled-banner')
.remove();
return e.preventDefault(); return e.preventDefault();
}); });
Project.projectSelectDropdown(); Project.projectSelectDropdown();
......
<script> <script>
import projectFeatureSetting from './project_feature_setting.vue'; import projectFeatureSetting from './project_feature_setting.vue';
import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue'; import projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external'; import { toggleHiddenClassBySelector } from '../external';
export default { export default {
components: { components: {
projectFeatureSetting, projectFeatureSetting,
projectFeatureToggle, projectFeatureToggle,
projectSettingRow, projectSettingRow,
}, },
props: { props: {
currentSettings: { currentSettings: {
type: Object, type: Object,
required: true, required: true,
}, },
canChangeVisibilityLevel: { canChangeVisibilityLevel: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
allowedVisibilityOptions: { allowedVisibilityOptions: {
type: Array, type: Array,
required: false, required: false,
default: () => [0, 10, 20], default: () => [0, 10, 20],
}, },
lfsAvailable: { lfsAvailable: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
registryAvailable: { registryAvailable: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
visibilityHelpPath: { visibilityHelpPath: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
},
lfsHelpPath: {
type: String,
required: false,
default: '',
},
registryHelpPath: {
type: String,
required: false,
default: '',
},
pagesAvailable: {
type: Boolean,
required: false,
default: false,
},
pagesAccessControlEnabled: {
type: Boolean,
required: false,
default: false,
},
pagesHelpPath: {
type: String,
required: false,
default: '',
},
}, },
lfsHelpPath: {
type: String,
required: false,
default: '',
},
registryHelpPath: {
type: String,
required: false,
default: '',
},
pagesAvailable: {
type: Boolean,
required: false,
default: false,
},
pagesAccessControlEnabled: {
type: Boolean,
required: false,
default: false,
},
pagesHelpPath: {
type: String,
required: false,
default: '',
},
},
data() { data() {
const defaults = { const defaults = {
visibilityOptions, visibilityOptions,
visibilityLevel: visibilityOptions.PUBLIC, visibilityLevel: visibilityOptions.PUBLIC,
issuesAccessLevel: 20, issuesAccessLevel: 20,
repositoryAccessLevel: 20, repositoryAccessLevel: 20,
mergeRequestsAccessLevel: 20, mergeRequestsAccessLevel: 20,
buildsAccessLevel: 20, buildsAccessLevel: 20,
wikiAccessLevel: 20, wikiAccessLevel: 20,
snippetsAccessLevel: 20, snippetsAccessLevel: 20,
pagesAccessLevel: 20, pagesAccessLevel: 20,
containerRegistryEnabled: true, containerRegistryEnabled: true,
lfsEnabled: true, lfsEnabled: true,
requestAccessEnabled: true, requestAccessEnabled: true,
highlightChangesClass: false, highlightChangesClass: false,
}; };
return { ...defaults, ...this.currentSettings }; return { ...defaults, ...this.currentSettings };
}, },
computed: { computed: {
featureAccessLevelOptions() { featureAccessLevelOptions() {
const options = [ const options = [[10, 'Only Project Members']];
[10, 'Only Project Members'], if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
]; options.push([20, 'Everyone With Access']);
if (this.visibilityLevel !== visibilityOptions.PRIVATE) { }
options.push([20, 'Everyone With Access']); return options;
} },
return options;
},
repoFeatureAccessLevelOptions() { repoFeatureAccessLevelOptions() {
return this.featureAccessLevelOptions.filter( return this.featureAccessLevelOptions.filter(
([value]) => value <= this.repositoryAccessLevel, ([value]) => value <= this.repositoryAccessLevel,
); );
}, },
pagesFeatureAccessLevelOptions() { pagesFeatureAccessLevelOptions() {
if (this.visibilityLevel !== visibilityOptions.PUBLIC) { if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
return this.featureAccessLevelOptions.concat([[30, 'Everyone']]); return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
} }
return this.featureAccessLevelOptions; return this.featureAccessLevelOptions;
}, },
repositoryEnabled() { repositoryEnabled() {
return this.repositoryAccessLevel > 0; return this.repositoryAccessLevel > 0;
}, },
visibilityLevelDescription() { visibilityLevelDescription() {
return visibilityLevelDescriptions[this.visibilityLevel]; return visibilityLevelDescriptions[this.visibilityLevel];
},
}, },
},
watch: { watch: {
visibilityLevel(value, oldValue) { visibilityLevel(value, oldValue) {
if (value === visibilityOptions.PRIVATE) { if (value === visibilityOptions.PRIVATE) {
// when private, features are restricted to "only team members" // when private, features are restricted to "only team members"
this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
if (this.pagesAccessLevel === 20) { if (this.pagesAccessLevel === 20) {
// When from Internal->Private narrow access for only members // When from Internal->Private narrow access for only members
this.pagesAccessLevel = 10; this.pagesAccessLevel = 10;
}
this.highlightChanges();
} else if (oldValue === visibilityOptions.PRIVATE) {
// if changing away from private, make enabled features more permissive
if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
this.highlightChanges();
} }
}, this.highlightChanges();
} else if (oldValue === visibilityOptions.PRIVATE) {
// if changing away from private, make enabled features more permissive
if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
this.highlightChanges();
}
},
repositoryAccessLevel(value, oldValue) { repositoryAccessLevel(value, oldValue) {
if (value < oldValue) { if (value < oldValue) {
// sub-features cannot have more premissive access level // sub-features cannot have more premissive access level
this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
if (value === 0) { if (value === 0) {
this.containerRegistryEnabled = false; this.containerRegistryEnabled = false;
this.lfsEnabled = false; this.lfsEnabled = false;
}
} else if (oldValue === 0) {
this.mergeRequestsAccessLevel = value;
this.buildsAccessLevel = value;
this.containerRegistryEnabled = true;
this.lfsEnabled = true;
} }
}, } else if (oldValue === 0) {
this.mergeRequestsAccessLevel = value;
this.buildsAccessLevel = value;
this.containerRegistryEnabled = true;
this.lfsEnabled = true;
}
},
issuesAccessLevel(value, oldValue) { issuesAccessLevel(value, oldValue) {
if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
}, },
mergeRequestsAccessLevel(value, oldValue) { mergeRequestsAccessLevel(value, oldValue) {
if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
}, },
buildsAccessLevel(value, oldValue) { buildsAccessLevel(value, oldValue) {
if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
},
}, },
},
methods: { methods: {
highlightChanges() { highlightChanges() {
this.highlightChangesClass = true; this.highlightChangesClass = true;
this.$nextTick(() => { this.$nextTick(() => {
this.highlightChangesClass = false; this.highlightChangesClass = false;
}); });
}, },
visibilityAllowed(option) { visibilityAllowed(option) {
return this.allowedVisibilityOptions.includes(option); return this.allowedVisibilityOptions.includes(option);
},
}, },
}; },
};
</script> </script>
<template> <template>
......
...@@ -4,8 +4,10 @@ import { slugifyWithHyphens } from '../lib/utils/text_utility'; ...@@ -4,8 +4,10 @@ import { slugifyWithHyphens } from '../lib/utils/text_utility';
let hasUserDefinedProjectPath = false; let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl) => { const deriveProjectPathFromUrl = $projectImportUrl => {
const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path'); const $currentProjectPath = $projectImportUrl
.parents('.toggle-import-form')
.find('#project_path');
if (hasUserDefinedProjectPath) { if (hasUserDefinedProjectPath) {
return; return;
} }
...@@ -52,9 +54,11 @@ const bindEvents = () => { ...@@ -52,9 +54,11 @@ const bindEvents = () => {
return; return;
} }
$('.how_to_import_link').on('click', (e) => { $('.how_to_import_link').on('click', e => {
e.preventDefault(); e.preventDefault();
$(e.currentTarget).next('.modal').show(); $(e.currentTarget)
.next('.modal')
.show();
}); });
$('.modal-header .close').on('click', () => { $('.modal-header .close').on('click', () => {
...@@ -63,15 +67,21 @@ const bindEvents = () => { ...@@ -63,15 +67,21 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').on('click', () => { $('.btn_import_gitlab_project').on('click', () => {
const importHref = $('a.btn_import_gitlab_project').attr('href'); const importHref = $('a.btn_import_gitlab_project').attr('href');
$('.btn_import_gitlab_project') $('.btn_import_gitlab_project').attr(
.attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&name=${$projectName.val()}&path=${$projectPath.val()}`); 'href',
`${importHref}?namespace_id=${$(
'#project_namespace_id',
).val()}&name=${$projectName.val()}&path=${$projectPath.val()}`,
);
}); });
if ($pushNewProjectTipTrigger) { if ($pushNewProjectTipTrigger) {
$pushNewProjectTipTrigger $pushNewProjectTipTrigger
.removeAttr('rel') .removeAttr('rel')
.removeAttr('target') .removeAttr('target')
.on('click', (e) => { e.preventDefault(); }) .on('click', e => {
e.preventDefault();
})
.popover({ .popover({
title: $pushNewProjectTipTrigger.data('title'), title: $pushNewProjectTipTrigger.data('title'),
placement: 'bottom', placement: 'bottom',
...@@ -79,13 +89,15 @@ const bindEvents = () => { ...@@ -79,13 +89,15 @@ const bindEvents = () => {
content: $('.push-new-project-tip-template').html(), content: $('.push-new-project-tip-template').html(),
}) })
.on('shown.bs.popover', () => { .on('shown.bs.popover', () => {
$(document).on('click.popover touchstart.popover', (event) => { $(document).on('click.popover touchstart.popover', event => {
if ($(event.target).closest('.popover').length === 0) { if ($(event.target).closest('.popover').length === 0) {
$pushNewProjectTipTrigger.trigger('click'); $pushNewProjectTipTrigger.trigger('click');
} }
}); });
const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus'); const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find(
'.js-select-on-focus',
);
addSelectOnFocusBehaviour(target); addSelectOnFocusBehaviour(target);
target.focus(); target.focus();
...@@ -117,16 +129,18 @@ const bindEvents = () => { ...@@ -117,16 +129,18 @@ const bindEvents = () => {
const selectedTemplate = templates[value]; const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text); $selectedTemplateText.text(selectedTemplate.text);
$(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon); $(selectedTemplate.icon)
.clone()
.addClass('d-block')
.appendTo($selectedIcon);
const $activeTabProjectName = $('.tab-pane.active #project_name'); const $activeTabProjectName = $('.tab-pane.active #project_name');
const $activeTabProjectPath = $('.tab-pane.active #project_path'); const $activeTabProjectPath = $('.tab-pane.active #project_path');
$activeTabProjectName.focus(); $activeTabProjectName.focus();
$activeTabProjectName $activeTabProjectName.keyup(() => {
.keyup(() => { onProjectNameChange($activeTabProjectName, $activeTabProjectPath);
onProjectNameChange($activeTabProjectName, $activeTabProjectPath); hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0;
hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0; });
});
} }
$useTemplateBtn.on('change', chooseTemplate); $useTemplateBtn.on('change', chooseTemplate);
......
...@@ -21,7 +21,7 @@ Sidebar.initialize = function(currentUser) { ...@@ -21,7 +21,7 @@ Sidebar.initialize = function(currentUser) {
} }
}; };
Sidebar.prototype.removeListeners = function () { Sidebar.prototype.removeListeners = function() {
this.sidebar.off('click', '.sidebar-collapsed-icon'); this.sidebar.off('click', '.sidebar-collapsed-icon');
this.sidebar.off('hidden.gl.dropdown'); this.sidebar.off('hidden.gl.dropdown');
$('.dropdown').off('loading.gl.dropdown'); $('.dropdown').off('loading.gl.dropdown');
...@@ -38,10 +38,12 @@ Sidebar.prototype.addEventListeners = function() { ...@@ -38,10 +38,12 @@ Sidebar.prototype.addEventListeners = function() {
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked); $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); return $(document)
.off('click', '.js-issuable-todo')
.on('click', '.js-issuable-todo', this.toggleTodo);
}; };
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
var $allGutterToggleIcons, $this, isExpanded, tooltipLabel; var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault(); e.preventDefault();
$this = $(this); $this = $(this);
...@@ -51,18 +53,26 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { ...@@ -51,18 +53,26 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (isExpanded) { if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); $('aside.right-sidebar')
$('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); .removeClass('right-sidebar-expanded')
.addClass('right-sidebar-collapsed');
$('.layout-page')
.removeClass('right-sidebar-expanded')
.addClass('right-sidebar-collapsed');
} else { } else {
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('aside.right-sidebar')
$('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); .removeClass('right-sidebar-collapsed')
.addClass('right-sidebar-expanded');
$('.layout-page')
.removeClass('right-sidebar-collapsed')
.addClass('right-sidebar-expanded');
} }
$this.attr('data-original-title', tooltipLabel); $this.attr('data-original-title', tooltipLabel);
if (!triggered) { if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); Cookies.set('collapsed_gutter', $('.right-sidebar').hasClass('right-sidebar-collapsed'));
} }
}; };
...@@ -71,21 +81,27 @@ Sidebar.prototype.toggleTodo = function(e) { ...@@ -71,21 +81,27 @@ Sidebar.prototype.toggleTodo = function(e) {
$this = $(e.currentTarget); $this = $(e.currentTarget);
ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post'; ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post';
if ($this.attr('data-delete-path')) { if ($this.attr('data-delete-path')) {
url = "" + ($this.attr('data-delete-path')); url = '' + $this.attr('data-delete-path');
} else { } else {
url = "" + ($this.data('url')); url = '' + $this.data('url');
} }
$this.tooltip('hide'); $this.tooltip('hide');
$('.js-issuable-todo').disable().addClass('is-loading'); $('.js-issuable-todo')
.disable()
.addClass('is-loading');
axios[ajaxType](url, { axios[ajaxType](url, {
issuable_id: $this.data('issuableId'), issuable_id: $this.data('issuableId'),
issuable_type: $this.data('issuableType'), issuable_type: $this.data('issuableType'),
}).then(({ data }) => { })
this.todoUpdateDone(data); .then(({ data }) => {
}).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`)); this.todoUpdateDone(data);
})
.catch(() =>
flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`),
);
}; };
Sidebar.prototype.todoUpdateDone = function(data) { Sidebar.prototype.todoUpdateDone = function(data) {
...@@ -99,7 +115,8 @@ Sidebar.prototype.todoUpdateDone = function(data) { ...@@ -99,7 +115,8 @@ Sidebar.prototype.todoUpdateDone = function(data) {
const $el = $(el); const $el = $(el);
const $elText = $el.find('.js-issuable-todo-inner'); const $elText = $el.find('.js-issuable-todo-inner');
$el.removeClass('is-loading') $el
.removeClass('is-loading')
.enable() .enable()
.attr('aria-label', $el.data(`${attrPrefix}Text`)) .attr('aria-label', $el.data(`${attrPrefix}Text`))
.attr('data-delete-path', deletePath) .attr('data-delete-path', deletePath)
...@@ -119,7 +136,9 @@ Sidebar.prototype.todoUpdateDone = function(data) { ...@@ -119,7 +136,9 @@ Sidebar.prototype.todoUpdateDone = function(data) {
Sidebar.prototype.sidebarDropdownLoading = function(e) { Sidebar.prototype.sidebarDropdownLoading = function(e) {
var $loading, $sidebarCollapsedIcon, i, img; var $loading, $sidebarCollapsedIcon, i, img;
$sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); $sidebarCollapsedIcon = $(this)
.closest('.block')
.find('.sidebar-collapsed-icon');
img = $sidebarCollapsedIcon.find('img'); img = $sidebarCollapsedIcon.find('img');
i = $sidebarCollapsedIcon.find('i'); i = $sidebarCollapsedIcon.find('i');
$loading = $('<i class="fa fa-spinner fa-spin"></i>'); $loading = $('<i class="fa fa-spinner fa-spin"></i>');
...@@ -134,7 +153,9 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) { ...@@ -134,7 +153,9 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) {
Sidebar.prototype.sidebarDropdownLoaded = function(e) { Sidebar.prototype.sidebarDropdownLoaded = function(e) {
var $sidebarCollapsedIcon, i, img; var $sidebarCollapsedIcon, i, img;
$sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); $sidebarCollapsedIcon = $(this)
.closest('.block')
.find('.sidebar-collapsed-icon');
img = $sidebarCollapsedIcon.find('img'); img = $sidebarCollapsedIcon.find('img');
$sidebarCollapsedIcon.find('i.fa-spin').remove(); $sidebarCollapsedIcon.find('i.fa-spin').remove();
i = $sidebarCollapsedIcon.find('i'); i = $sidebarCollapsedIcon.find('i');
...@@ -220,7 +241,7 @@ Sidebar.prototype.isOpen = function() { ...@@ -220,7 +241,7 @@ Sidebar.prototype.isOpen = function() {
}; };
Sidebar.prototype.getBlock = function(name) { Sidebar.prototype.getBlock = function(name) {
return this.sidebar.find(".block." + name); return this.sidebar.find('.block.' + name);
}; };
export default Sidebar; export default Sidebar;
...@@ -234,7 +234,9 @@ export class SearchAutocomplete { ...@@ -234,7 +234,9 @@ export class SearchAutocomplete {
icon, icon,
text: term, text: term,
template, template,
url: `${gon.relative_url_root}/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`, url: `${
gon.relative_url_root
}/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
}); });
} }
} }
......
...@@ -74,8 +74,8 @@ export default { ...@@ -74,8 +74,8 @@ export default {
} }
if (!this.users.length) { if (!this.users.length) {
const emptyTooltipLabel = this.issuableType === 'issue' ? const emptyTooltipLabel =
__('Assignee(s)') : __('Assignee'); this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee');
names.push(emptyTooltipLabel); names.push(emptyTooltipLabel);
} }
...@@ -248,4 +248,3 @@ export default { ...@@ -248,4 +248,3 @@ export default {
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue'; import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
const ICON_ON = 'notifications'; const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off'; const ICON_OFF = 'notifications-off';
const LABEL_ON = __('Notifications on'); const LABEL_ON = __('Notifications on');
const LABEL_OFF = __('Notifications off'); const LABEL_OFF = __('Notifications off');
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
components: {
icon,
toggleButton,
},
props: {
loading: {
type: Boolean,
required: false,
default: false,
}, },
components: { subscribed: {
icon, type: Boolean,
toggleButton, required: false,
default: null,
}, },
props: { id: {
loading: { type: Number,
type: Boolean, required: false,
required: false, default: null,
default: false,
},
subscribed: {
type: Boolean,
required: false,
default: null,
},
id: {
type: Number,
required: false,
default: null,
},
}, },
computed: { },
showLoadingState() { computed: {
return this.subscribed === null; showLoadingState() {
}, return this.subscribed === null;
notificationIcon() {
return this.subscribed ? ICON_ON : ICON_OFF;
},
notificationTooltip() {
return this.subscribed ? LABEL_ON : LABEL_OFF;
},
}, },
methods: { notificationIcon() {
/** return this.subscribed ? ICON_ON : ICON_OFF;
* We need to emit this event on both component & eventHub },
* for 2 dependencies; notificationTooltip() {
* return this.subscribed ? LABEL_ON : LABEL_OFF;
* 1. eventHub: This component is used in Issue Boards sidebar },
* where component template is part of HAML },
* and event listeners are tied to app's eventHub. methods: {
* 2. Component: This compone is also used in Epics in EE /**
* where listeners are tied to component event. * We need to emit this event on both component & eventHub
*/ * for 2 dependencies;
toggleSubscription() { *
// App's eventHub event emission. * 1. eventHub: This component is used in Issue Boards sidebar
eventHub.$emit('toggleSubscription', this.id); * where component template is part of HAML
* and event listeners are tied to app's eventHub.
* 2. Component: This compone is also used in Epics in EE
* where listeners are tied to component event.
*/
toggleSubscription() {
// App's eventHub event emission.
eventHub.$emit('toggleSubscription', this.id);
// Component event emission. // Component event emission.
this.$emit('toggleSubscription', this.id); this.$emit('toggleSubscription', this.id);
}, },
onClickCollapsedIcon() { onClickCollapsedIcon() {
this.$emit('toggleSidebar'); this.$emit('toggleSidebar');
},
}, },
}; },
};
</script> </script>
<template> <template>
......
...@@ -39,9 +39,10 @@ export default class SidebarMediator { ...@@ -39,9 +39,10 @@ export default class SidebarMediator {
} }
fetch() { fetch() {
return this.service.get() return this.service
.get()
.then(response => response.json()) .then(response => response.json())
.then((data) => { .then(data => {
this.processFetchedData(data); this.processFetchedData(data);
}) })
.catch(() => new Flash('Error occurred when fetching sidebar data')); .catch(() => new Flash('Error occurred when fetching sidebar data'));
...@@ -56,30 +57,33 @@ export default class SidebarMediator { ...@@ -56,30 +57,33 @@ export default class SidebarMediator {
toggleSubscription() { toggleSubscription() {
this.store.setFetchingState('subscriptions', true); this.store.setFetchingState('subscriptions', true);
return this.service.toggleSubscription() return this.service
.toggleSubscription()
.then(() => { .then(() => {
this.store.setSubscribedState(!this.store.subscribed); this.store.setSubscribedState(!this.store.subscribed);
this.store.setFetchingState('subscriptions', false); this.store.setFetchingState('subscriptions', false);
}) })
.catch((err) => { .catch(err => {
this.store.setFetchingState('subscriptions', false); this.store.setFetchingState('subscriptions', false);
throw err; throw err;
}); });
} }
fetchAutocompleteProjects(searchTerm) { fetchAutocompleteProjects(searchTerm) {
return this.service.getProjectsAutocomplete(searchTerm) return this.service
.getProjectsAutocomplete(searchTerm)
.then(response => response.json()) .then(response => response.json())
.then((data) => { .then(data => {
this.store.setAutocompleteProjects(data); this.store.setAutocompleteProjects(data);
return this.store.autocompleteProjects; return this.store.autocompleteProjects;
}); });
} }
moveIssue() { moveIssue() {
return this.service.moveIssue(this.store.moveToProjectId) return this.service
.moveIssue(this.store.moveToProjectId)
.then(response => response.json()) .then(response => response.json())
.then((data) => { .then(data => {
if (window.location.pathname !== data.web_url) { if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url); visitUrl(data.web_url);
} }
......
...@@ -63,10 +63,15 @@ export default { ...@@ -63,10 +63,15 @@ export default {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0; return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
}, },
errorText() { errorText() {
return sprintf(__('Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}'), { return sprintf(
linkStart: `<a href="${this.troubleshootingDocsPath}">`, __(
linkEnd: '</a>', 'Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}',
}); ),
{
linkStart: `<a href="${this.troubleshootingDocsPath}">`,
linkEnd: '</a>',
},
);
}, },
}, },
}; };
......
...@@ -71,7 +71,12 @@ export default { ...@@ -71,7 +71,12 @@ export default {
return defaultClass; return defaultClass;
}, },
iconClass() { iconClass() {
if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) { if (
this.status === 'failed' ||
!this.commitMessage.length ||
!this.mr.isMergeAllowed ||
this.mr.preventMerge
) {
return 'warning'; return 'warning';
} }
return 'success'; return 'success';
...@@ -90,10 +95,12 @@ export default { ...@@ -90,10 +95,12 @@ export default {
}, },
isMergeButtonDisabled() { isMergeButtonDisabled() {
const { commitMessage } = this; const { commitMessage } = this;
return Boolean(!commitMessage.length return Boolean(
|| !this.shouldShowMergeControls() !commitMessage.length ||
|| this.isMakingRequest !this.shouldShowMergeControls() ||
|| this.mr.preventMerge); this.isMakingRequest ||
this.mr.preventMerge,
);
}, },
isRemoveSourceBranchButtonDisabled() { isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled; return this.isMergeButtonDisabled;
...@@ -140,9 +147,10 @@ export default { ...@@ -140,9 +147,10 @@ export default {
}; };
this.isMakingRequest = true; this.isMakingRequest = true;
this.service.merge(options) this.service
.merge(options)
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
const hasError = data.status === 'failed' || data.status === 'hook_validation_error'; const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
if (data.status === 'merge_when_pipeline_succeeds') { if (data.status === 'merge_when_pipeline_succeeds') {
...@@ -167,9 +175,10 @@ export default { ...@@ -167,9 +175,10 @@ export default {
}); });
}, },
handleMergePolling(continuePolling, stopPolling) { handleMergePolling(continuePolling, stopPolling) {
this.service.poll() this.service
.poll()
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
if (data.state === 'merged') { if (data.state === 'merged') {
// If state is merged we should update the widget and stop the polling // If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('MRWidgetUpdateRequested');
...@@ -205,9 +214,10 @@ export default { ...@@ -205,9 +214,10 @@ export default {
}); });
}, },
handleRemoveBranchPolling(continuePolling, stopPolling) { handleRemoveBranchPolling(continuePolling, stopPolling) {
this.service.poll() this.service
.poll()
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
// If source branch exists then we should continue polling // If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time // because removing a source branch is a background task and takes time
if (data.source_branch_exists) { if (data.source_branch_exists) {
......
...@@ -116,7 +116,7 @@ export default { ...@@ -116,7 +116,7 @@ export default {
// init polling // init polling
this.initPostMergeDeploymentsPolling(); this.initPostMergeDeploymentsPolling();
} }
} },
}, },
created() { created() {
this.initPolling(); this.initPolling();
...@@ -213,17 +213,21 @@ export default { ...@@ -213,17 +213,21 @@ export default {
}) })
.catch(() => this.throwDeploymentsError()); .catch(() => this.throwDeploymentsError());
}, },
fetchPostMergeDeployments(){ fetchPostMergeDeployments() {
return this.fetchDeployments('merge_commit') return this.fetchDeployments('merge_commit')
.then(({ data }) => { .then(({ data }) => {
if (data.length) { if (data.length) {
this.mr.postMergeDeployments = data; this.mr.postMergeDeployments = data;
} }
}) })
.catch(() => this.throwDeploymentsError()); .catch(() => this.throwDeploymentsError());
}, },
throwDeploymentsError() { throwDeploymentsError() {
createFlash(__('Something went wrong while fetching the environments for this merge request. Please try again.')); createFlash(
__(
'Something went wrong while fetching the environments for this merge request. Please try again.',
),
);
}, },
fetchActionsContent() { fetchActionsContent() {
this.service this.service
......
...@@ -24,8 +24,8 @@ export default class MRWidgetService { ...@@ -24,8 +24,8 @@ export default class MRWidgetService {
fetchDeployments(targetParam) { fetchDeployments(targetParam) {
return axios.get(this.endpoints.ciEnvironmentsStatusPath, { return axios.get(this.endpoints.ciEnvironmentsStatusPath, {
params: { params: {
environment_target: targetParam environment_target: targetParam,
} },
}); });
} }
......
...@@ -18,8 +18,7 @@ export default class MergeRequestStore { ...@@ -18,8 +18,7 @@ export default class MergeRequestStore {
this.squash = data.squash; this.squash = data.squash;
this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath =
this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path; this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path;
this.troubleshootingDocsPath = this.troubleshootingDocsPath = this.troubleshootingDocsPath || data.troubleshooting_docs_path;
this.troubleshootingDocsPath || data.troubleshooting_docs_path;
this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true; this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true;
this.iid = data.iid; this.iid = data.iid;
......
...@@ -56,12 +56,14 @@ export default { ...@@ -56,12 +56,14 @@ export default {
filteredResults() { filteredResults() {
if (this.filter !== '') { if (this.filter !== '') {
return this.items.filter( return this.items.filter(
item => item[this.filterKey] && item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()), item =>
item[this.filterKey] &&
item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()),
); );
} }
return this.items.slice(0, this.visibleItems); return this.items.slice(0, this.visibleItems);
} },
}, },
mounted() { mounted() {
/** /**
......
<script> <script>
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'CollapsedCalendarIcon', name: 'CollapsedCalendarIcon',
directives: { directives: {
tooltip, tooltip,
},
props: {
containerClass: {
type: String,
required: false,
default: '',
}, },
props: { text: {
containerClass: { type: String,
type: String, required: false,
required: false, default: '',
default: '',
},
text: {
type: String,
required: false,
default: '',
},
showIcon: {
type: Boolean,
required: false,
default: true,
},
tooltipText: {
type: String,
required: false,
default: '',
},
}, },
methods: { showIcon: {
click() { type: Boolean,
this.$emit('click'); required: false,
}, default: true,
}, },
}; tooltipText: {
type: String,
required: false,
default: '',
},
},
methods: {
click() {
this.$emit('click');
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { dateInWords, timeFor } from '~/lib/utils/datetime_utility'; import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
export default { export default {
name: 'SidebarCollapsedGroupedDatePicker', name: 'SidebarCollapsedGroupedDatePicker',
components: { components: {
collapsedCalendarIcon, collapsedCalendarIcon,
},
mixins: [timeagoMixin],
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
}, },
mixins: [ minDate: {
timeagoMixin, type: Date,
], required: false,
props: { default: null,
collapsed: {
type: Boolean,
required: false,
default: true,
},
minDate: {
type: Date,
required: false,
default: null,
},
maxDate: {
type: Date,
required: false,
default: null,
},
disableClickableIcons: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { maxDate: {
hasMinAndMaxDates() { type: Date,
return this.minDate && this.maxDate; required: false,
}, default: null,
hasNoMinAndMaxDates() {
return !this.minDate && !this.maxDate;
},
showMinDateBlock() {
return this.minDate || this.hasNoMinAndMaxDates;
},
showFromText() {
return !this.maxDate && this.minDate;
},
iconClass() {
const disabledClass = this.disableClickableIcons ? 'disabled' : '';
return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
},
}, },
methods: { disableClickableIcons: {
toggleSidebar() { type: Boolean,
this.$emit('toggleCollapse'); required: false,
}, default: false,
dateText(dateType = 'min') { },
const date = this[`${dateType}Date`]; },
const dateWords = dateInWords(date, true); computed: {
const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords; hasMinAndMaxDates() {
return this.minDate && this.maxDate;
},
hasNoMinAndMaxDates() {
return !this.minDate && !this.maxDate;
},
showMinDateBlock() {
return this.minDate || this.hasNoMinAndMaxDates;
},
showFromText() {
return !this.maxDate && this.minDate;
},
iconClass() {
const disabledClass = this.disableClickableIcons ? 'disabled' : '';
return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
},
},
methods: {
toggleSidebar() {
this.$emit('toggleCollapse');
},
dateText(dateType = 'min') {
const date = this[`${dateType}Date`];
const dateWords = dateInWords(date, true);
const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
return date ? parsedDateWords : __('None'); return date ? parsedDateWords : __('None');
}, },
tooltipText(dateType = 'min') { tooltipText(dateType = 'min') {
const defaultText = dateType === 'min' ? __('Start date') : __('Due date'); const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
const date = this[`${dateType}Date`]; const date = this[`${dateType}Date`];
const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date); const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date);
const dateText = date ? [ const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : '';
this.dateText(dateType),
`(${timeAgo})`,
].join(' ') : '';
if (date) { if (date) {
return [defaultText, dateText].join('<br />'); return [defaultText, dateText].join('<br />');
} }
return __('Start and due date'); return __('Start and due date');
},
}, },
}; },
};
</script> </script>
<template> <template>
......
...@@ -14,7 +14,10 @@ export default { ...@@ -14,7 +14,10 @@ export default {
}, },
computed: { computed: {
labelsList() { labelsList() {
const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', '); const labelsString = this.labels
.slice(0, 5)
.map(label => label.title)
.join(', ');
if (this.labels.length > 5) { if (this.labels.length > 5) {
return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), { return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
......
...@@ -18,7 +18,7 @@ describe('Board list component', () => { ...@@ -18,7 +18,7 @@ describe('Board list component', () => {
let mock; let mock;
let component; let component;
beforeEach((done) => { beforeEach(done => {
const el = document.createElement('div'); const el = document.createElement('div');
document.body.appendChild(el); document.body.appendChild(el);
...@@ -62,122 +62,102 @@ describe('Board list component', () => { ...@@ -62,122 +62,102 @@ describe('Board list component', () => {
}); });
it('renders component', () => { it('renders component', () => {
expect( expect(component.$el.classList.contains('board-list-component')).toBe(true);
component.$el.classList.contains('board-list-component'),
).toBe(true);
}); });
it('renders loading icon', (done) => { it('renders loading icon', done => {
component.loading = true; component.loading = true;
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
component.$el.querySelector('.board-list-loading'),
).not.toBeNull();
done(); done();
}); });
}); });
it('renders issues', () => { it('renders issues', () => {
expect( expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
component.$el.querySelectorAll('.board-card').length,
).toBe(1);
}); });
it('sets data attribute with issue id', () => { it('sets data attribute with issue id', () => {
expect( expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
component.$el.querySelector('.board-card').getAttribute('data-issue-id'),
).toBe('1');
}); });
it('shows new issue form', (done) => { it('shows new issue form', done => {
component.toggleForm(); component.toggleForm();
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
component.$el.querySelector('.board-new-issue-form'),
).not.toBeNull();
expect( expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
component.$el.querySelector('.is-smaller'),
).not.toBeNull();
done(); done();
}); });
}); });
it('shows new issue form after eventhub event', (done) => { it('shows new issue form after eventhub event', done => {
eventHub.$emit(`hide-issue-form-${component.list.id}`); eventHub.$emit(`hide-issue-form-${component.list.id}`);
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
component.$el.querySelector('.board-new-issue-form'),
).not.toBeNull();
expect( expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
component.$el.querySelector('.is-smaller'),
).not.toBeNull();
done(); done();
}); });
}); });
it('does not show new issue form for closed list', (done) => { it('does not show new issue form for closed list', done => {
component.list.type = 'closed'; component.list.type = 'closed';
component.toggleForm(); component.toggleForm();
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
component.$el.querySelector('.board-new-issue-form'),
).toBeNull();
done(); done();
}); });
}); });
it('shows count list item', (done) => { it('shows count list item', done => {
component.showCount = true; component.showCount = true;
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
component.$el.querySelector('.board-list-count'),
).not.toBeNull();
expect( expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
component.$el.querySelector('.board-list-count').textContent.trim(), 'Showing all issues',
).toBe('Showing all issues'); );
done(); done();
}); });
}); });
it('sets data attribute with invalid id', (done) => { it('sets data attribute with invalid id', done => {
component.showCount = true; component.showCount = true;
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
component.$el.querySelector('.board-list-count').getAttribute('data-issue-id'), '-1',
).toBe('-1'); );
done(); done();
}); });
}); });
it('shows how many more issues to load', (done) => { it('shows how many more issues to load', done => {
component.showCount = true; component.showCount = true;
component.list.issuesSize = 20; component.list.issuesSize = 20;
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
component.$el.querySelector('.board-list-count').textContent.trim(), 'Showing 1 of 20 issues',
).toBe('Showing 1 of 20 issues'); );
done(); done();
}); });
}); });
it('loads more issues after scrolling', (done) => { it('loads more issues after scrolling', done => {
spyOn(component.list, 'nextPage'); spyOn(component.list, 'nextPage');
component.$refs.list.style.height = '100px'; component.$refs.list.style.height = '100px';
component.$refs.list.style.overflow = 'scroll'; component.$refs.list.style.overflow = 'scroll';
...@@ -200,7 +180,9 @@ describe('Board list component', () => { ...@@ -200,7 +180,9 @@ describe('Board list component', () => {
}); });
it('does not load issues if already loading', () => { it('does not load issues if already loading', () => {
component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(new Promise(() => {})); component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(
new Promise(() => {}),
);
component.onScroll(); component.onScroll();
component.onScroll(); component.onScroll();
...@@ -208,14 +190,12 @@ describe('Board list component', () => { ...@@ -208,14 +190,12 @@ describe('Board list component', () => {
expect(component.list.nextPage).toHaveBeenCalledTimes(1); expect(component.list.nextPage).toHaveBeenCalledTimes(1);
}); });
it('shows loading more spinner', (done) => { it('shows loading more spinner', done => {
component.showCount = true; component.showCount = true;
component.list.loadingMore = true; component.list.loadingMore = true;
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(component.$el.querySelector('.board-list-count .fa-spinner')).not.toBeNull();
component.$el.querySelector('.board-list-count .fa-spinner'),
).not.toBeNull();
done(); done();
}); });
......
...@@ -8,7 +8,7 @@ describe('Board component', () => { ...@@ -8,7 +8,7 @@ describe('Board component', () => {
let vm; let vm;
let el; let el;
beforeEach((done) => { beforeEach(done => {
loadFixtures('boards/show.html.raw'); loadFixtures('boards/show.html.raw');
el = document.createElement('div'); el = document.createElement('div');
...@@ -50,56 +50,46 @@ describe('Board component', () => { ...@@ -50,56 +50,46 @@ describe('Board component', () => {
}); });
it('board is expandable when list type is backlog', () => { it('board is expandable when list type is backlog', () => {
expect( expect(vm.$el.classList.contains('is-expandable')).toBe(true);
vm.$el.classList.contains('is-expandable'),
).toBe(true);
}); });
it('board is expandable when list type is closed', (done) => { it('board is expandable when list type is closed', done => {
vm.list.type = 'closed'; vm.list.type = 'closed';
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.$el.classList.contains('is-expandable')).toBe(true);
vm.$el.classList.contains('is-expandable'),
).toBe(true);
done(); done();
}); });
}); });
it('board is not expandable when list type is label', (done) => { it('board is not expandable when list type is label', done => {
vm.list.type = 'label'; vm.list.type = 'label';
vm.list.isExpandable = false; vm.list.isExpandable = false;
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.$el.classList.contains('is-expandable')).toBe(false);
vm.$el.classList.contains('is-expandable'),
).toBe(false);
done(); done();
}); });
}); });
it('collapses when clicking header', (done) => { it('collapses when clicking header', done => {
vm.$el.querySelector('.board-header').click(); vm.$el.querySelector('.board-header').click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
vm.$el.classList.contains('is-collapsed'),
).toBe(true);
done(); done();
}); });
}); });
it('created sets isExpanded to true from localStorage', (done) => { it('created sets isExpanded to true from localStorage', done => {
vm.$el.querySelector('.board-header').click(); vm.$el.querySelector('.board-header').click();
return Vue.nextTick() return Vue.nextTick()
.then(() => { .then(() => {
expect( expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
vm.$el.classList.contains('is-collapsed'),
).toBe(true);
// call created manually // call created manually
vm.$options.created[0].call(vm); vm.$options.created[0].call(vm);
...@@ -107,11 +97,10 @@ describe('Board component', () => { ...@@ -107,11 +97,10 @@ describe('Board component', () => {
return Vue.nextTick(); return Vue.nextTick();
}) })
.then(() => { .then(() => {
expect( expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
vm.$el.classList.contains('is-collapsed'),
).toBe(true);
done(); done();
}).catch(done.fail); })
.catch(done.fail);
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import emptyState from '~/environments/components/empty_state.vue'; import emptyState from '~/environments/components/empty_state.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
...@@ -25,13 +24,13 @@ describe('environments empty state', () => { ...@@ -25,13 +24,13 @@ describe('environments empty state', () => {
}); });
it('renders empty state and new environment button', () => { it('renders empty state and new environment button', () => {
expect( expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual(
vm.$el.querySelector('.js-blank-state-title').textContent.trim(), "You don't have any environments right now",
).toEqual('You don\'t have any environments right now'); );
expect( expect(vm.$el.querySelector('.js-new-environment-button').getAttribute('href')).toEqual(
vm.$el.querySelector('.js-new-environment-button').getAttribute('href'), 'foo',
).toEqual('foo'); );
}); });
}); });
...@@ -45,13 +44,11 @@ describe('environments empty state', () => { ...@@ -45,13 +44,11 @@ describe('environments empty state', () => {
}); });
it('renders empty state without new button', () => { it('renders empty state without new button', () => {
expect( expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual(
vm.$el.querySelector('.js-blank-state-title').textContent.trim(), "You don't have any environments right now",
).toEqual('You don\'t have any environments right now'); );
expect( expect(vm.$el.querySelector('.js-new-environment-button')).toBeNull();
vm.$el.querySelector('.js-new-environment-button'),
).toBeNull();
}); });
}); });
}); });
...@@ -38,7 +38,9 @@ describe('Environment item', () => { ...@@ -38,7 +38,9 @@ describe('Environment item', () => {
}); });
it('Should render the number of children in a badge', () => { it('Should render the number of children in a badge', () => {
expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size); expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(
mockItem.size,
);
}); });
}); });
...@@ -68,7 +70,8 @@ describe('Environment item', () => { ...@@ -68,7 +70,8 @@ describe('Environment item', () => {
username: 'root', username: 'root',
id: 1, id: 1,
state: 'active', state: 'active',
avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', avatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root', web_url: 'http://localhost:3000/root',
}, },
commit: { commit: {
...@@ -84,7 +87,8 @@ describe('Environment item', () => { ...@@ -84,7 +87,8 @@ describe('Environment item', () => {
username: 'root', username: 'root',
id: 1, id: 1,
state: 'active', state: 'active',
avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', avatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root', web_url: 'http://localhost:3000/root',
}, },
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
...@@ -121,18 +125,18 @@ describe('Environment item', () => { ...@@ -121,18 +125,18 @@ describe('Environment item', () => {
}); });
it('should render environment name', () => { it('should render environment name', () => {
expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name); expect(component.$el.querySelector('.environment-name').textContent).toContain(
environment.name,
);
}); });
describe('With deployment', () => { describe('With deployment', () => {
it('should render deployment internal id', () => { it('should render deployment internal id', () => {
expect( expect(component.$el.querySelector('.deployment-column span').textContent).toContain(
component.$el.querySelector('.deployment-column span').textContent, environment.last_deployment.iid,
).toContain(environment.last_deployment.iid); );
expect( expect(component.$el.querySelector('.deployment-column span').textContent).toContain('#');
component.$el.querySelector('.deployment-column span').textContent,
).toContain('#');
}); });
it('should render last deployment date', () => { it('should render last deployment date', () => {
...@@ -156,56 +160,46 @@ describe('Environment item', () => { ...@@ -156,56 +160,46 @@ describe('Environment item', () => {
describe('With build url', () => { describe('With build url', () => {
it('Should link to build url provided', () => { it('Should link to build url provided', () => {
expect( expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual(
component.$el.querySelector('.build-link').getAttribute('href'), environment.last_deployment.deployable.build_path,
).toEqual(environment.last_deployment.deployable.build_path); );
}); });
it('Should render deployable name and id', () => { it('Should render deployable name and id', () => {
expect( expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual(
component.$el.querySelector('.build-link').getAttribute('href'), environment.last_deployment.deployable.build_path,
).toEqual(environment.last_deployment.deployable.build_path); );
}); });
}); });
describe('With commit information', () => { describe('With commit information', () => {
it('should render commit component', () => { it('should render commit component', () => {
expect( expect(component.$el.querySelector('.js-commit-component')).toBeDefined();
component.$el.querySelector('.js-commit-component'),
).toBeDefined();
}); });
}); });
}); });
describe('With manual actions', () => { describe('With manual actions', () => {
it('Should render actions component', () => { it('Should render actions component', () => {
expect( expect(component.$el.querySelector('.js-manual-actions-container')).toBeDefined();
component.$el.querySelector('.js-manual-actions-container'),
).toBeDefined();
}); });
}); });
describe('With external URL', () => { describe('With external URL', () => {
it('should render external url component', () => { it('should render external url component', () => {
expect( expect(component.$el.querySelector('.js-external-url-container')).toBeDefined();
component.$el.querySelector('.js-external-url-container'),
).toBeDefined();
}); });
}); });
describe('With stop action', () => { describe('With stop action', () => {
it('Should render stop action component', () => { it('Should render stop action component', () => {
expect( expect(component.$el.querySelector('.js-stop-component-container')).toBeDefined();
component.$el.querySelector('.js-stop-component-container'),
).toBeDefined();
}); });
}); });
describe('With retry action', () => { describe('With retry action', () => {
it('Should render rollback component', () => { it('Should render rollback component', () => {
expect( expect(component.$el.querySelector('.js-rollback-component-container')).toBeDefined();
component.$el.querySelector('.js-rollback-component-container'),
).toBeDefined();
}); });
}); });
}); });
......
...@@ -33,7 +33,7 @@ describe('Environment', () => { ...@@ -33,7 +33,7 @@ describe('Environment', () => {
describe('successfull request', () => { describe('successfull request', () => {
describe('without environments', () => { describe('without environments', () => {
beforeEach((done) => { beforeEach(done => {
mock.onGet(mockData.endpoint).reply(200, { environments: [] }); mock.onGet(mockData.endpoint).reply(200, { environments: [] });
component = mountComponent(EnvironmentsComponent, mockData); component = mountComponent(EnvironmentsComponent, mockData);
...@@ -44,30 +44,34 @@ describe('Environment', () => { ...@@ -44,30 +44,34 @@ describe('Environment', () => {
}); });
it('should render the empty state', () => { it('should render the empty state', () => {
expect( expect(component.$el.querySelector('.js-new-environment-button').textContent).toContain(
component.$el.querySelector('.js-new-environment-button').textContent, 'New environment',
).toContain('New environment'); );
expect( expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
component.$el.querySelector('.js-blank-state-title').textContent, "You don't have any environments right now",
).toContain('You don\'t have any environments right now'); );
}); });
}); });
describe('with paginated environments', () => { describe('with paginated environments', () => {
beforeEach((done) => { beforeEach(done => {
mock.onGet(mockData.endpoint).reply(200, { mock.onGet(mockData.endpoint).reply(
environments: [environment], 200,
stopped_count: 1, {
available_count: 0, environments: [environment],
}, { stopped_count: 1,
'X-nExt-pAge': '2', available_count: 0,
'x-page': '1', },
'X-Per-Page': '1', {
'X-Prev-Page': '', 'X-nExt-pAge': '2',
'X-TOTAL': '37', 'x-page': '1',
'X-Total-Pages': '2', 'X-Per-Page': '1',
}); 'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
);
component = mountComponent(EnvironmentsComponent, mockData); component = mountComponent(EnvironmentsComponent, mockData);
...@@ -78,19 +82,17 @@ describe('Environment', () => { ...@@ -78,19 +82,17 @@ describe('Environment', () => {
it('should render a table with environments', () => { it('should render a table with environments', () => {
expect(component.$el.querySelectorAll('table')).not.toBeNull(); expect(component.$el.querySelectorAll('table')).not.toBeNull();
expect( expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual(
component.$el.querySelector('.environment-name').textContent.trim(), environment.name,
).toEqual(environment.name); );
}); });
describe('pagination', () => { describe('pagination', () => {
it('should render pagination', () => { it('should render pagination', () => {
expect( expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5);
component.$el.querySelectorAll('.gl-pagination li').length,
).toEqual(5);
}); });
it('should make an API request when page is clicked', (done) => { it('should make an API request when page is clicked', done => {
spyOn(component, 'updateContent'); spyOn(component, 'updateContent');
setTimeout(() => { setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
...@@ -100,7 +102,7 @@ describe('Environment', () => { ...@@ -100,7 +102,7 @@ describe('Environment', () => {
}, 0); }, 0);
}); });
it('should make an API request when using tabs', (done) => { it('should make an API request when using tabs', done => {
setTimeout(() => { setTimeout(() => {
spyOn(component, 'updateContent'); spyOn(component, 'updateContent');
component.$el.querySelector('.js-environments-tab-stopped').click(); component.$el.querySelector('.js-environments-tab-stopped').click();
...@@ -114,7 +116,7 @@ describe('Environment', () => { ...@@ -114,7 +116,7 @@ describe('Environment', () => {
}); });
describe('unsuccessfull request', () => { describe('unsuccessfull request', () => {
beforeEach((done) => { beforeEach(done => {
mock.onGet(mockData.endpoint).reply(500, {}); mock.onGet(mockData.endpoint).reply(500, {});
component = mountComponent(EnvironmentsComponent, mockData); component = mountComponent(EnvironmentsComponent, mockData);
...@@ -125,15 +127,16 @@ describe('Environment', () => { ...@@ -125,15 +127,16 @@ describe('Environment', () => {
}); });
it('should render empty state', () => { it('should render empty state', () => {
expect( expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
component.$el.querySelector('.js-blank-state-title').textContent, "You don't have any environments right now",
).toContain('You don\'t have any environments right now'); );
}); });
}); });
describe('expandable folders', () => { describe('expandable folders', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(mockData.endpoint).reply(200, mock.onGet(mockData.endpoint).reply(
200,
{ {
environments: [folder], environments: [folder],
stopped_count: 0, stopped_count: 0,
...@@ -154,7 +157,7 @@ describe('Environment', () => { ...@@ -154,7 +157,7 @@ describe('Environment', () => {
component = mountComponent(EnvironmentsComponent, mockData); component = mountComponent(EnvironmentsComponent, mockData);
}); });
it('should open a closed folder', (done) => { it('should open a closed folder', done => {
setTimeout(() => { setTimeout(() => {
component.$el.querySelector('.folder-name').click(); component.$el.querySelector('.folder-name').click();
...@@ -165,7 +168,7 @@ describe('Environment', () => { ...@@ -165,7 +168,7 @@ describe('Environment', () => {
}, 0); }, 0);
}); });
it('should close an opened folder', (done) => { it('should close an opened folder', done => {
setTimeout(() => { setTimeout(() => {
// open folder // open folder
component.$el.querySelector('.folder-name').click(); component.$el.querySelector('.folder-name').click();
...@@ -182,7 +185,7 @@ describe('Environment', () => { ...@@ -182,7 +185,7 @@ describe('Environment', () => {
}, 0); }, 0);
}); });
it('should show children environments and a button to show all environments', (done) => { it('should show children environments and a button to show all environments', done => {
setTimeout(() => { setTimeout(() => {
// open folder // open folder
component.$el.querySelector('.folder-name').click(); component.$el.querySelector('.folder-name').click();
...@@ -191,7 +194,9 @@ describe('Environment', () => { ...@@ -191,7 +194,9 @@ describe('Environment', () => {
// wait for next async request // wait for next async request
setTimeout(() => { setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1); expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all'); expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain(
'Show all',
);
done(); done();
}); });
}); });
...@@ -201,7 +206,8 @@ describe('Environment', () => { ...@@ -201,7 +206,8 @@ describe('Environment', () => {
describe('methods', () => { describe('methods', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(mockData.endpoint).reply(200, mock.onGet(mockData.endpoint).reply(
200,
{ {
environments: [], environments: [],
stopped_count: 0, stopped_count: 0,
...@@ -215,8 +221,9 @@ describe('Environment', () => { ...@@ -215,8 +221,9 @@ describe('Environment', () => {
}); });
describe('updateContent', () => { describe('updateContent', () => {
it('should set given parameters', (done) => { it('should set given parameters', done => {
component.updateContent({ scope: 'stopped', page: '3' }) component
.updateContent({ scope: 'stopped', page: '3' })
.then(() => { .then(() => {
expect(component.page).toEqual('3'); expect(component.page).toEqual('3');
expect(component.scope).toEqual('stopped'); expect(component.scope).toEqual('stopped');
......
...@@ -32,37 +32,41 @@ describe('Environments Folder View', () => { ...@@ -32,37 +32,41 @@ describe('Environments Folder View', () => {
describe('successfull request', () => { describe('successfull request', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(mockData.endpoint).reply(200, { mock.onGet(mockData.endpoint).reply(
environments: environmentsList, 200,
stopped_count: 1, {
available_count: 0, environments: environmentsList,
}, { stopped_count: 1,
'X-nExt-pAge': '2', available_count: 0,
'x-page': '1', },
'X-Per-Page': '2', {
'X-Prev-Page': '', 'X-nExt-pAge': '2',
'X-TOTAL': '20', 'x-page': '1',
'X-Total-Pages': '10', 'X-Per-Page': '2',
}); 'X-Prev-Page': '',
'X-TOTAL': '20',
'X-Total-Pages': '10',
},
);
component = mountComponent(Component, mockData); component = mountComponent(Component, mockData);
}); });
it('should render a table with environments', (done) => { it('should render a table with environments', done => {
setTimeout(() => { setTimeout(() => {
expect(component.$el.querySelectorAll('table')).not.toBeNull(); expect(component.$el.querySelectorAll('table')).not.toBeNull();
expect( expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual(
component.$el.querySelector('.environment-name').textContent.trim(), environmentsList[0].name,
).toEqual(environmentsList[0].name); );
done(); done();
}, 0); }, 0);
}); });
it('should render available tab with count', (done) => { it('should render available tab with count', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain(
component.$el.querySelector('.js-environments-tab-available').textContent, 'Available',
).toContain('Available'); );
expect( expect(
component.$el.querySelector('.js-environments-tab-available .badge').textContent, component.$el.querySelector('.js-environments-tab-available .badge').textContent,
...@@ -71,11 +75,11 @@ describe('Environments Folder View', () => { ...@@ -71,11 +75,11 @@ describe('Environments Folder View', () => {
}, 0); }, 0);
}); });
it('should render stopped tab with count', (done) => { it('should render stopped tab with count', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain(
component.$el.querySelector('.js-environments-tab-stopped').textContent, 'Stopped',
).toContain('Stopped'); );
expect( expect(
component.$el.querySelector('.js-environments-tab-stopped .badge').textContent, component.$el.querySelector('.js-environments-tab-stopped .badge').textContent,
...@@ -84,36 +88,37 @@ describe('Environments Folder View', () => { ...@@ -84,36 +88,37 @@ describe('Environments Folder View', () => {
}, 0); }, 0);
}); });
it('should render parent folder name', (done) => { it('should render parent folder name', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(component.$el.querySelector('.js-folder-name').textContent.trim()).toContain(
component.$el.querySelector('.js-folder-name').textContent.trim(), 'Environments / review',
).toContain('Environments / review'); );
done(); done();
}, 0); }, 0);
}); });
describe('pagination', () => { describe('pagination', () => {
it('should render pagination', (done) => { it('should render pagination', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(component.$el.querySelectorAll('.gl-pagination')).not.toBeNull();
component.$el.querySelectorAll('.gl-pagination'),
).not.toBeNull();
done(); done();
}, 0); }, 0);
}); });
it('should make an API request when changing page', (done) => { it('should make an API request when changing page', done => {
spyOn(component, 'updateContent'); spyOn(component, 'updateContent');
setTimeout(() => { setTimeout(() => {
component.$el.querySelector('.gl-pagination .js-last-button a').click(); component.$el.querySelector('.gl-pagination .js-last-button a').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '10' }); expect(component.updateContent).toHaveBeenCalledWith({
scope: component.scope,
page: '10',
});
done(); done();
}, 0); }, 0);
}); });
it('should make an API request when using tabs', (done) => { it('should make an API request when using tabs', done => {
setTimeout(() => { setTimeout(() => {
spyOn(component, 'updateContent'); spyOn(component, 'updateContent');
component.$el.querySelector('.js-environments-tab-stopped').click(); component.$el.querySelector('.js-environments-tab-stopped').click();
...@@ -134,20 +139,18 @@ describe('Environments Folder View', () => { ...@@ -134,20 +139,18 @@ describe('Environments Folder View', () => {
component = mountComponent(Component, mockData); component = mountComponent(Component, mockData);
}); });
it('should not render a table', (done) => { it('should not render a table', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(component.$el.querySelector('table')).toBe(null);
component.$el.querySelector('table'),
).toBe(null);
done(); done();
}, 0); }, 0);
}); });
it('should render available tab with count 0', (done) => { it('should render available tab with count 0', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain(
component.$el.querySelector('.js-environments-tab-available').textContent, 'Available',
).toContain('Available'); );
expect( expect(
component.$el.querySelector('.js-environments-tab-available .badge').textContent, component.$el.querySelector('.js-environments-tab-available .badge').textContent,
...@@ -156,11 +159,11 @@ describe('Environments Folder View', () => { ...@@ -156,11 +159,11 @@ describe('Environments Folder View', () => {
}, 0); }, 0);
}); });
it('should render stopped tab with count 0', (done) => { it('should render stopped tab with count 0', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain(
component.$el.querySelector('.js-environments-tab-stopped').textContent, 'Stopped',
).toContain('Stopped'); );
expect( expect(
component.$el.querySelector('.js-environments-tab-stopped .badge').textContent, component.$el.querySelector('.js-environments-tab-stopped .badge').textContent,
...@@ -181,8 +184,9 @@ describe('Environments Folder View', () => { ...@@ -181,8 +184,9 @@ describe('Environments Folder View', () => {
}); });
describe('updateContent', () => { describe('updateContent', () => {
it('should set given parameters', (done) => { it('should set given parameters', done => {
component.updateContent({ scope: 'stopped', page: '4' }) component
.updateContent({ scope: 'stopped', page: '4' })
.then(() => { .then(() => {
expect(component.page).toEqual('4'); expect(component.page).toEqual('4');
expect(component.scope).toEqual('stopped'); expect(component.scope).toEqual('stopped');
......
...@@ -25,25 +25,21 @@ describe('Title component', () => { ...@@ -25,25 +25,21 @@ describe('Title component', () => {
}); });
it('renders title HTML', () => { it('renders title HTML', () => {
expect( expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
vm.$el.querySelector('.title').innerHTML.trim(),
).toBe('Testing <img>');
}); });
it('updates page title when changing titleHtml', (done) => { it('updates page title when changing titleHtml', done => {
spyOn(vm, 'setPageTitle'); spyOn(vm, 'setPageTitle');
vm.titleHtml = 'test'; vm.titleHtml = 'test';
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.setPageTitle).toHaveBeenCalled();
vm.setPageTitle,
).toHaveBeenCalled();
done(); done();
}); });
}); });
it('animates title changes', (done) => { it('animates title changes', done => {
vm.titleHtml = 'test'; vm.titleHtml = 'test';
Vue.nextTick(() => { Vue.nextTick(() => {
...@@ -61,14 +57,12 @@ describe('Title component', () => { ...@@ -61,14 +57,12 @@ describe('Title component', () => {
}); });
}); });
it('updates page title after changing title', (done) => { it('updates page title after changing title', done => {
vm.titleHtml = 'changed'; vm.titleHtml = 'changed';
vm.titleText = 'changed'; vm.titleText = 'changed';
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(document.querySelector('title').textContent.trim()).toContain('changed');
document.querySelector('title').textContent.trim(),
).toContain('changed');
done(); done();
}); });
......
...@@ -234,7 +234,7 @@ describe('Job App ', () => { ...@@ -234,7 +234,7 @@ describe('Job App ', () => {
); );
done(); done();
}, 0); }, 0);
}) });
}); });
it('does not renders stuck block when there are no runners', done => { it('does not renders stuck block when there are no runners', done => {
......
...@@ -68,41 +68,20 @@ describe('Job State actions', () => { ...@@ -68,41 +68,20 @@ describe('Job State actions', () => {
describe('hideSidebar', () => { describe('hideSidebar', () => {
it('should commit HIDE_SIDEBAR mutation', done => { it('should commit HIDE_SIDEBAR mutation', done => {
testAction( testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done);
hideSidebar,
null,
mockedState,
[{ type: types.HIDE_SIDEBAR }],
[],
done,
);
}); });
}); });
describe('showSidebar', () => { describe('showSidebar', () => {
it('should commit HIDE_SIDEBAR mutation', done => { it('should commit HIDE_SIDEBAR mutation', done => {
testAction( testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], [], done);
showSidebar,
null,
mockedState,
[{ type: types.SHOW_SIDEBAR }],
[],
done,
);
}); });
}); });
describe('toggleSidebar', () => { describe('toggleSidebar', () => {
describe('when isSidebarOpen is true', () => { describe('when isSidebarOpen is true', () => {
it('should dispatch hideSidebar', done => { it('should dispatch hideSidebar', done => {
testAction( testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }], done);
toggleSidebar,
null,
mockedState,
[],
[{ type: 'hideSidebar' }],
done,
);
}); });
}); });
...@@ -110,14 +89,7 @@ describe('Job State actions', () => { ...@@ -110,14 +89,7 @@ describe('Job State actions', () => {
it('should dispatch showSidebar', done => { it('should dispatch showSidebar', done => {
mockedState.isSidebarOpen = false; mockedState.isSidebarOpen = false;
testAction( testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }], done);
toggleSidebar,
null,
mockedState,
[],
[{ type: 'showSidebar' }],
done,
);
}); });
}); });
}); });
......
...@@ -180,7 +180,7 @@ describe('Job Store Getters', () => { ...@@ -180,7 +180,7 @@ describe('Job Store Getters', () => {
it('returns true', () => { it('returns true', () => {
localState.job.runners = { localState.job.runners = {
available: true, available: true,
online: false online: false,
}; };
expect(getters.hasRunnersForProject(localState)).toEqual(true); expect(getters.hasRunnersForProject(localState)).toEqual(true);
...@@ -191,7 +191,7 @@ describe('Job Store Getters', () => { ...@@ -191,7 +191,7 @@ describe('Job Store Getters', () => {
it('returns false', () => { it('returns false', () => {
localState.job.runners = { localState.job.runners = {
available: false, available: false,
online: false online: false,
}; };
expect(getters.hasRunnersForProject(localState)).toEqual(false); expect(getters.hasRunnersForProject(localState)).toEqual(false);
...@@ -202,7 +202,7 @@ describe('Job Store Getters', () => { ...@@ -202,7 +202,7 @@ describe('Job Store Getters', () => {
it('returns false', () => { it('returns false', () => {
localState.job.runners = { localState.job.runners = {
available: false, available: false,
online: true online: true,
}; };
expect(getters.hasRunnersForProject(localState)).toEqual(false); expect(getters.hasRunnersForProject(localState)).toEqual(false);
......
...@@ -35,9 +35,7 @@ describe('common_utils', () => { ...@@ -35,9 +35,7 @@ describe('common_utils', () => {
}); });
it('should decode params', () => { it('should decode params', () => {
expect( expect(commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test');
commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0],
).toBe('label_name[]=test');
}); });
it('should remove the question mark from the search params', () => { it('should remove the question mark from the search params', () => {
...@@ -49,25 +47,19 @@ describe('common_utils', () => { ...@@ -49,25 +47,19 @@ describe('common_utils', () => {
describe('urlParamsToObject', () => { describe('urlParamsToObject', () => {
it('parses path for label with trailing +', () => { it('parses path for label with trailing +', () => {
expect( expect(commonUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({
commonUtils.urlParamsToObject('label_name[]=label%2B', {}),
).toEqual({
label_name: ['label+'], label_name: ['label+'],
}); });
}); });
it('parses path for milestone with trailing +', () => { it('parses path for milestone with trailing +', () => {
expect( expect(commonUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({
commonUtils.urlParamsToObject('milestone_title=A%2B', {}),
).toEqual({
milestone_title: 'A+', milestone_title: 'A+',
}); });
}); });
it('parses path for search terms with spaces', () => { it('parses path for search terms with spaces', () => {
expect( expect(commonUtils.urlParamsToObject('search=two+words', {})).toEqual({
commonUtils.urlParamsToObject('search=two+words', {}),
).toEqual({
search: 'two words', search: 'two words',
}); });
}); });
...@@ -187,7 +179,10 @@ describe('common_utils', () => { ...@@ -187,7 +179,10 @@ describe('common_utils', () => {
describe('parseQueryStringIntoObject', () => { describe('parseQueryStringIntoObject', () => {
it('should return object with query parameters', () => { it('should return object with query parameters', () => {
expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' }); expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({
scope: 'all',
page: '2',
});
expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' }); expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' });
expect(commonUtils.parseQueryStringIntoObject()).toEqual({}); expect(commonUtils.parseQueryStringIntoObject()).toEqual({});
}); });
...@@ -211,7 +206,9 @@ describe('common_utils', () => { ...@@ -211,7 +206,9 @@ describe('common_utils', () => {
describe('buildUrlWithCurrentLocation', () => { describe('buildUrlWithCurrentLocation', () => {
it('should build an url with current location and given parameters', () => { it('should build an url with current location and given parameters', () => {
expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname);
expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`); expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(
`${window.location.pathname}?page=2`,
);
}); });
}); });
...@@ -266,21 +263,24 @@ describe('common_utils', () => { ...@@ -266,21 +263,24 @@ describe('common_utils', () => {
}); });
describe('normalizeCRLFHeaders', () => { describe('normalizeCRLFHeaders', () => {
beforeEach(function () { beforeEach(function() {
this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; this.CLRFHeaders =
'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE';
spyOn(String.prototype, 'split').and.callThrough(); spyOn(String.prototype, 'split').and.callThrough();
this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders); this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders);
}); });
it('should split by newline', function () { it('should split by newline', function() {
expect(String.prototype.split).toHaveBeenCalledWith('\n'); expect(String.prototype.split).toHaveBeenCalledWith('\n');
}); });
it('should split by colon+space for each header', function () { it('should split by colon+space for each header', function() {
expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(
3,
);
}); });
it('should return a normalized headers object', function () { it('should return a normalized headers object', function() {
expect(this.normalizeCRLFHeaders).toEqual({ expect(this.normalizeCRLFHeaders).toEqual({
'A-HEADER': 'a-value', 'A-HEADER': 'a-value',
'ANOTHER-HEADER': 'ANOTHER-VALUE', 'ANOTHER-HEADER': 'ANOTHER-VALUE',
...@@ -359,67 +359,79 @@ describe('common_utils', () => { ...@@ -359,67 +359,79 @@ describe('common_utils', () => {
spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0)); spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0));
}); });
it('solves the promise from the callback', (done) => { it('solves the promise from the callback', done => {
const expectedResponseValue = 'Success!'; const expectedResponseValue = 'Success!';
commonUtils.backOff((next, stop) => ( commonUtils
new Promise((resolve) => { .backOff((next, stop) =>
resolve(expectedResponseValue); new Promise(resolve => {
}).then((resp) => { resolve(expectedResponseValue);
stop(resp); })
.then(resp => {
stop(resp);
})
.catch(done.fail),
)
.then(respBackoff => {
expect(respBackoff).toBe(expectedResponseValue);
done();
}) })
).catch(done.fail)).then((respBackoff) => { .catch(done.fail);
expect(respBackoff).toBe(expectedResponseValue);
done();
}).catch(done.fail);
}); });
it('catches the rejected promise from the callback ', (done) => { it('catches the rejected promise from the callback ', done => {
const errorMessage = 'Mistakes were made!'; const errorMessage = 'Mistakes were made!';
commonUtils.backOff((next, stop) => { commonUtils
new Promise((resolve, reject) => { .backOff((next, stop) => {
reject(new Error(errorMessage)); new Promise((resolve, reject) => {
}).then((resp) => { reject(new Error(errorMessage));
stop(resp); })
}).catch(err => stop(err)); .then(resp => {
}).catch((errBackoffResp) => { stop(resp);
expect(errBackoffResp instanceof Error).toBe(true); })
expect(errBackoffResp.message).toBe(errorMessage); .catch(err => stop(err));
done(); })
}); .catch(errBackoffResp => {
expect(errBackoffResp instanceof Error).toBe(true);
expect(errBackoffResp.message).toBe(errorMessage);
done();
});
}); });
it('solves the promise correctly after retrying a third time', (done) => { it('solves the promise correctly after retrying a third time', done => {
let numberOfCalls = 1; let numberOfCalls = 1;
const expectedResponseValue = 'Success!'; const expectedResponseValue = 'Success!';
commonUtils.backOff((next, stop) => ( commonUtils
Promise.resolve(expectedResponseValue) .backOff((next, stop) =>
.then((resp) => { Promise.resolve(expectedResponseValue)
if (numberOfCalls < 3) { .then(resp => {
numberOfCalls += 1; if (numberOfCalls < 3) {
next(); numberOfCalls += 1;
} else { next();
stop(resp); } else {
} stop(resp);
}) }
).catch(done.fail)).then((respBackoff) => { })
const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); .catch(done.fail),
)
.then(respBackoff => {
const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
expect(timeouts).toEqual([2000, 4000]); expect(timeouts).toEqual([2000, 4000]);
expect(respBackoff).toBe(expectedResponseValue); expect(respBackoff).toBe(expectedResponseValue);
done(); done();
}).catch(done.fail); })
.catch(done.fail);
}); });
it('rejects the backOff promise after timing out', (done) => { it('rejects the backOff promise after timing out', done => {
commonUtils.backOff(next => next(), 64000) commonUtils.backOff(next => next(), 64000).catch(errBackoffResp => {
.catch((errBackoffResp) => { const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]);
expect(errBackoffResp instanceof Error).toBe(true); expect(errBackoffResp instanceof Error).toBe(true);
expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT');
done(); done();
}); });
}); });
}); });
...@@ -466,11 +478,14 @@ describe('common_utils', () => { ...@@ -466,11 +478,14 @@ describe('common_utils', () => {
}); });
describe('createOverlayIcon', () => { describe('createOverlayIcon', () => {
it('should return the favicon with the overlay', (done) => { it('should return the favicon with the overlay', done => {
commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => { commonUtils
expect(url).toEqual(faviconWithOverlayDataUrl); .createOverlayIcon(faviconDataUrl, overlayDataUrl)
done(); .then(url => {
}).catch(done.fail); expect(url).toEqual(faviconWithOverlayDataUrl);
done();
})
.catch(done.fail);
}); });
}); });
...@@ -486,11 +501,16 @@ describe('common_utils', () => { ...@@ -486,11 +501,16 @@ describe('common_utils', () => {
document.body.removeChild(document.getElementById('favicon')); document.body.removeChild(document.getElementById('favicon'));
}); });
it('should set page favicon to provided favicon overlay', (done) => { it('should set page favicon to provided favicon overlay', done => {
commonUtils.setFaviconOverlay(overlayDataUrl).then(() => { commonUtils
expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl); .setFaviconOverlay(overlayDataUrl)
done(); .then(() => {
}).catch(done.fail); expect(document.getElementById('favicon').getAttribute('href')).toEqual(
faviconWithOverlayDataUrl,
);
done();
})
.catch(done.fail);
}); });
}); });
...@@ -512,24 +532,24 @@ describe('common_utils', () => { ...@@ -512,24 +532,24 @@ describe('common_utils', () => {
document.body.removeChild(document.getElementById('favicon')); document.body.removeChild(document.getElementById('favicon'));
}); });
it('should reset favicon in case of error', (done) => { it('should reset favicon in case of error', done => {
mock.onGet(BUILD_URL).replyOnce(500); mock.onGet(BUILD_URL).replyOnce(500);
commonUtils.setCiStatusFavicon(BUILD_URL) commonUtils.setCiStatusFavicon(BUILD_URL).catch(() => {
.catch(() => { const favicon = document.getElementById('favicon');
const favicon = document.getElementById('favicon');
expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); expect(favicon.getAttribute('href')).toEqual(faviconDataUrl);
done(); done();
}); });
}); });
it('should set page favicon to CI status favicon based on provided status', (done) => { it('should set page favicon to CI status favicon based on provided status', done => {
mock.onGet(BUILD_URL).reply(200, { mock.onGet(BUILD_URL).reply(200, {
favicon: overlayDataUrl, favicon: overlayDataUrl,
}); });
commonUtils.setCiStatusFavicon(BUILD_URL) commonUtils
.setCiStatusFavicon(BUILD_URL)
.then(() => { .then(() => {
const favicon = document.getElementById('favicon'); const favicon = document.getElementById('favicon');
...@@ -554,11 +574,15 @@ describe('common_utils', () => { ...@@ -554,11 +574,15 @@ describe('common_utils', () => {
}); });
it('should return the svg for a linked icon', () => { it('should return the svg for a linked icon', () => {
expect(commonUtils.spriteIcon('test')).toEqual('<svg ><use xlink:href="icons.svg#test" /></svg>'); expect(commonUtils.spriteIcon('test')).toEqual(
'<svg ><use xlink:href="icons.svg#test" /></svg>',
);
}); });
it('should set svg className when passed', () => { it('should set svg className when passed', () => {
expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>'); expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual(
'<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>',
);
}); });
}); });
...@@ -578,7 +602,7 @@ describe('common_utils', () => { ...@@ -578,7 +602,7 @@ describe('common_utils', () => {
const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj); const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj);
Object.keys(convertedObj).forEach((prop) => { Object.keys(convertedObj).forEach(prop => {
expect(snakeRegEx.test(prop)).toBeFalsy(); expect(snakeRegEx.test(prop)).toBeFalsy();
expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]); expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]);
}); });
...@@ -597,9 +621,7 @@ describe('common_utils', () => { ...@@ -597,9 +621,7 @@ describe('common_utils', () => {
}, },
}; };
expect( expect(commonUtils.convertObjectPropsToCamelCase(obj)).toEqual({
commonUtils.convertObjectPropsToCamelCase(obj),
).toEqual({
snakeKey: { snakeKey: {
child_snake_key: 'value', child_snake_key: 'value',
}, },
...@@ -614,9 +636,7 @@ describe('common_utils', () => { ...@@ -614,9 +636,7 @@ describe('common_utils', () => {
}, },
}; };
expect( expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({
commonUtils.convertObjectPropsToCamelCase(obj, { deep: true }),
).toEqual({
snakeKey: { snakeKey: {
childSnakeKey: 'value', childSnakeKey: 'value',
}, },
...@@ -630,9 +650,7 @@ describe('common_utils', () => { ...@@ -630,9 +650,7 @@ describe('common_utils', () => {
}, },
]; ];
expect( expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }),
).toEqual([
{ {
childSnakeKey: 'value', childSnakeKey: 'value',
}, },
...@@ -648,9 +666,7 @@ describe('common_utils', () => { ...@@ -648,9 +666,7 @@ describe('common_utils', () => {
], ],
]; ];
expect( expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }),
).toEqual([
[ [
{ {
childSnakeKey: 'value', childSnakeKey: 'value',
......
import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils'; import {
formatRelevantDigits,
bytesToKiB,
bytesToMiB,
bytesToGiB,
numberToHumanSize,
} from '~/lib/utils/number_utils';
describe('Number Utils', () => { describe('Number Utils', () => {
describe('formatRelevantDigits', () => { describe('formatRelevantDigits', () => {
......
...@@ -2,7 +2,7 @@ import Vue from 'vue'; ...@@ -2,7 +2,7 @@ import Vue from 'vue';
import GraphFlag from '~/monitoring/components/graph/flag.vue'; import GraphFlag from '~/monitoring/components/graph/flag.vue';
import { deploymentData } from '../mock_data'; import { deploymentData } from '../mock_data';
const createComponent = (propsData) => { const createComponent = propsData => {
const Component = Vue.extend(GraphFlag); const Component = Vue.extend(GraphFlag);
return new Component({ return new Component({
...@@ -51,8 +51,7 @@ describe('GraphFlag', () => { ...@@ -51,8 +51,7 @@ describe('GraphFlag', () => {
it('has a line at the currentXCoordinate', () => { it('has a line at the currentXCoordinate', () => {
component = createComponent(defaultValuesComponent); component = createComponent(defaultValuesComponent);
expect(component.$el.style.left) expect(component.$el.style.left).toEqual(`${70 + component.currentXCoordinate}px`);
.toEqual(`${70 + component.currentXCoordinate}px`);
}); });
describe('Deployment flag', () => { describe('Deployment flag', () => {
...@@ -62,9 +61,7 @@ describe('GraphFlag', () => { ...@@ -62,9 +61,7 @@ describe('GraphFlag', () => {
deploymentFlagData, deploymentFlagData,
}); });
expect( expect(deploymentFlagComponent.$el.querySelector('.popover-title')).toContainText('Deployed');
deploymentFlagComponent.$el.querySelector('.popover-title'),
).toContainText('Deployed');
}); });
it('contains the ref when a tag is available', () => { it('contains the ref when a tag is available', () => {
...@@ -78,13 +75,13 @@ describe('GraphFlag', () => { ...@@ -78,13 +75,13 @@ describe('GraphFlag', () => {
}, },
}); });
expect( expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), 'f5bcd1d9',
).toContainText('f5bcd1d9'); );
expect( expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), '1.0',
).toContainText('1.0'); );
}); });
it('does not contain the ref when a tag is unavailable', () => { it('does not contain the ref when a tag is unavailable', () => {
...@@ -98,13 +95,13 @@ describe('GraphFlag', () => { ...@@ -98,13 +95,13 @@ describe('GraphFlag', () => {
}, },
}); });
expect( expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), 'f5bcd1d9',
).toContainText('f5bcd1d9'); );
expect( expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).not.toContainText(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), '1.0',
).not.toContainText('1.0'); );
}); });
}); });
......
...@@ -11,11 +11,13 @@ describe('DiscussionFilter component', () => { ...@@ -11,11 +11,13 @@ describe('DiscussionFilter component', () => {
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
const discussions = [{ const discussions = [
...discussionMock, {
id: discussionMock.id, ...discussionMock,
notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], id: discussionMock.id,
}]; notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
},
];
const Component = Vue.extend(DiscussionFilter); const Component = Vue.extend(DiscussionFilter);
const defaultValue = discussionFiltersMock[0].value; const defaultValue = discussionFiltersMock[0].value;
...@@ -35,11 +37,15 @@ describe('DiscussionFilter component', () => { ...@@ -35,11 +37,15 @@ describe('DiscussionFilter component', () => {
}); });
it('renders the all filters', () => { it('renders the all filters', () => {
expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(discussionFiltersMock.length); expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(
discussionFiltersMock.length,
);
}); });
it('renders the default selected item', () => { it('renders the default selected item', () => {
expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(discussionFiltersMock[0].title); expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(
discussionFiltersMock[0].title,
);
}); });
it('updates to the selected item', () => { it('updates to the selected item', () => {
......
...@@ -97,7 +97,8 @@ describe('note_app', () => { ...@@ -97,7 +97,8 @@ describe('note_app', () => {
}); });
it('should render list of notes', done => { it('should render list of notes', done => {
const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[ const note =
mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
'/gitlab-org/gitlab-ce/issues/26/discussions.json' '/gitlab-org/gitlab-ce/issues/26/discussions.json'
][0].notes[0]; ][0].notes[0];
......
...@@ -40,7 +40,9 @@ describe('graph component', () => { ...@@ -40,7 +40,9 @@ describe('graph component', () => {
).toEqual(true); ).toEqual(true);
expect( expect(
component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'), component.$el
.querySelector('.stage-column:nth-child(2) .build:nth-child(1)')
.classList.contains('left-connector'),
).toEqual(true); ).toEqual(true);
expect(component.$el.querySelector('loading-icon')).toBe(null); expect(component.$el.querySelector('loading-icon')).toBe(null);
...@@ -56,7 +58,9 @@ describe('graph component', () => { ...@@ -56,7 +58,9 @@ describe('graph component', () => {
pipeline: graphJSON, pipeline: graphJSON,
}); });
expect(component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim()).toEqual('Deploy &lt;img src=x onerror=alert(document.domain)&gt;'); expect(
component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim(),
).toEqual('Deploy &lt;img src=x onerror=alert(document.domain)&gt;');
}); });
}); });
}); });
...@@ -78,9 +78,7 @@ describe('Assignee component', () => { ...@@ -78,9 +78,7 @@ describe('Assignee component', () => {
component = new AssigneeComponent({ component = new AssigneeComponent({
propsData: { propsData: {
rootPath: 'http://localhost:3000', rootPath: 'http://localhost:3000',
users: [ users: [UsersMock.user],
UsersMock.user,
],
editable: false, editable: false,
}, },
}).$mount(); }).$mount();
...@@ -90,7 +88,9 @@ describe('Assignee component', () => { ...@@ -90,7 +88,9 @@ describe('Assignee component', () => {
expect(collapsed.childElementCount).toEqual(1); expect(collapsed.childElementCount).toEqual(1);
expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar); expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar);
expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(`${UsersMock.user.name}'s avatar`); expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(
`${UsersMock.user.name}'s avatar`,
);
expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name); expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name);
}); });
...@@ -98,34 +98,38 @@ describe('Assignee component', () => { ...@@ -98,34 +98,38 @@ describe('Assignee component', () => {
component = new AssigneeComponent({ component = new AssigneeComponent({
propsData: { propsData: {
rootPath: 'http://localhost:3000/', rootPath: 'http://localhost:3000/',
users: [ users: [UsersMock.user],
UsersMock.user,
],
editable: true, editable: true,
}, },
}).$mount(); }).$mount();
expect(component.$el.querySelector('.author-link')).not.toBeNull(); expect(component.$el.querySelector('.author-link')).not.toBeNull();
// The image // The image
expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(UsersMock.user.avatar); expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(
UsersMock.user.avatar,
);
// Author name // Author name
expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(UsersMock.user.name); expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(
UsersMock.user.name,
);
// Username // Username
expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(`@${UsersMock.user.username}`); expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(
`@${UsersMock.user.username}`,
);
}); });
it('has the root url present in the assigneeUrl method', () => { it('has the root url present in the assigneeUrl method', () => {
component = new AssigneeComponent({ component = new AssigneeComponent({
propsData: { propsData: {
rootPath: 'http://localhost:3000/', rootPath: 'http://localhost:3000/',
users: [ users: [UsersMock.user],
UsersMock.user,
],
editable: true, editable: true,
}, },
}).$mount(); }).$mount();
expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(-1); expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(
-1,
);
}); });
}); });
...@@ -147,13 +151,17 @@ describe('Assignee component', () => { ...@@ -147,13 +151,17 @@ describe('Assignee component', () => {
const first = collapsed.children[0]; const first = collapsed.children[0];
expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar); expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`); expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(
`${users[0].name}'s avatar`,
);
expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name); expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
const second = collapsed.children[1]; const second = collapsed.children[1];
expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar); expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar);
expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[1].name}'s avatar`); expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(
`${users[1].name}'s avatar`,
);
expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name); expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name);
}); });
...@@ -174,7 +182,9 @@ describe('Assignee component', () => { ...@@ -174,7 +182,9 @@ describe('Assignee component', () => {
const first = collapsed.children[0]; const first = collapsed.children[0];
expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar); expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`); expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(
`${users[0].name}'s avatar`,
);
expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name); expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
const second = collapsed.children[1]; const second = collapsed.children[1];
...@@ -196,7 +206,7 @@ describe('Assignee component', () => { ...@@ -196,7 +206,7 @@ describe('Assignee component', () => {
expect(component.$el.querySelector('.user-list-more')).toBe(null); expect(component.$el.querySelector('.user-list-more')).toBe(null);
}); });
it('Shows the "show-less" assignees label', (done) => { it('Shows the "show-less" assignees label', done => {
const users = UsersMockHelper.createNumberRandomUsers(6); const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({ component = new AssigneeComponent({
propsData: { propsData: {
...@@ -206,21 +216,25 @@ describe('Assignee component', () => { ...@@ -206,21 +216,25 @@ describe('Assignee component', () => {
}, },
}).$mount(); }).$mount();
expect(component.$el.querySelectorAll('.user-item').length).toEqual(component.defaultRenderCount); expect(component.$el.querySelectorAll('.user-item').length).toEqual(
component.defaultRenderCount,
);
expect(component.$el.querySelector('.user-list-more')).not.toBe(null); expect(component.$el.querySelector('.user-list-more')).not.toBe(null);
const usersLabelExpectation = users.length - component.defaultRenderCount; const usersLabelExpectation = users.length - component.defaultRenderCount;
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe(
.not.toBe(`+${usersLabelExpectation} more`); `+${usersLabelExpectation} more`,
);
component.toggleShowLess(); component.toggleShowLess();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
.toBe('- show less'); '- show less',
);
done(); done();
}); });
}); });
it('Shows the "show-less" when "n+ more " label is clicked', (done) => { it('Shows the "show-less" when "n+ more " label is clicked', done => {
const users = UsersMockHelper.createNumberRandomUsers(6); const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({ component = new AssigneeComponent({
propsData: { propsData: {
...@@ -232,8 +246,9 @@ describe('Assignee component', () => { ...@@ -232,8 +246,9 @@ describe('Assignee component', () => {
component.$el.querySelector('.user-list-more .btn-link').click(); component.$el.querySelector('.user-list-more .btn-link').click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
.toBe('- show less'); '- show less',
);
done(); done();
}); });
}); });
...@@ -264,16 +279,18 @@ describe('Assignee component', () => { ...@@ -264,16 +279,18 @@ describe('Assignee component', () => {
}); });
it('shows "+1 more" label', () => { it('shows "+1 more" label', () => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
.toBe('+ 1 more'); '+ 1 more',
);
}); });
it('shows "show less" label', (done) => { it('shows "show less" label', done => {
component.toggleShowLess(); component.toggleShowLess();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
.toBe('- show less'); '- show less',
);
done(); done();
}); });
}); });
......
...@@ -69,12 +69,12 @@ describe('MRWidgetPipeline', () => { ...@@ -69,12 +69,12 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline, pipeline: mockData.pipeline,
hasCi: true, hasCi: true,
ciStatus: null, ciStatus: null,
troubleshootingDocsPath: 'help', troubleshootingDocsPath: 'help',
}); });
expect( expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
vm.$el.querySelector('.media-body').textContent.trim(), 'Could not retrieve the pipeline status. For troubleshooting steps, read the <a href="help">documentation.</a>',
).toContain('Could not retrieve the pipeline status. For troubleshooting steps, read the <a href="help">documentation.</a>'); );
}); });
describe('with a pipeline', () => { describe('with a pipeline', () => {
...@@ -88,34 +88,36 @@ describe('MRWidgetPipeline', () => { ...@@ -88,34 +88,36 @@ describe('MRWidgetPipeline', () => {
}); });
it('should render pipeline ID', () => { it('should render pipeline ID', () => {
expect( expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
vm.$el.querySelector('.pipeline-id').textContent.trim(), `#${mockData.pipeline.id}`,
).toEqual(`#${mockData.pipeline.id}`); );
}); });
it('should render pipeline status and commit id', () => { it('should render pipeline status and commit id', () => {
expect( expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
vm.$el.querySelector('.media-body').textContent.trim(), mockData.pipeline.details.status.label,
).toContain(mockData.pipeline.details.status.label); );
expect( expect(vm.$el.querySelector('.js-commit-link').textContent.trim()).toEqual(
vm.$el.querySelector('.js-commit-link').textContent.trim(), mockData.pipeline.commit.short_id,
).toEqual(mockData.pipeline.commit.short_id); );
expect( expect(vm.$el.querySelector('.js-commit-link').getAttribute('href')).toEqual(
vm.$el.querySelector('.js-commit-link').getAttribute('href'), mockData.pipeline.commit.commit_path,
).toEqual(mockData.pipeline.commit.commit_path); );
}); });
it('should render pipeline graph', () => { it('should render pipeline graph', () => {
expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length); expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(
mockData.pipeline.details.stages.length,
);
}); });
it('should render coverage information', () => { it('should render coverage information', () => {
expect( expect(vm.$el.querySelector('.media-body').textContent).toContain(
vm.$el.querySelector('.media-body').textContent, `Coverage ${mockData.pipeline.coverage}`,
).toContain(`Coverage ${mockData.pipeline.coverage}`); );
}); });
}); });
...@@ -133,30 +135,30 @@ describe('MRWidgetPipeline', () => { ...@@ -133,30 +135,30 @@ describe('MRWidgetPipeline', () => {
}); });
it('should render pipeline ID', () => { it('should render pipeline ID', () => {
expect( expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
vm.$el.querySelector('.pipeline-id').textContent.trim(), `#${mockData.pipeline.id}`,
).toEqual(`#${mockData.pipeline.id}`); );
}); });
it('should render pipeline status', () => { it('should render pipeline status', () => {
expect( expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
vm.$el.querySelector('.media-body').textContent.trim(), mockData.pipeline.details.status.label,
).toContain(mockData.pipeline.details.status.label); );
expect( expect(vm.$el.querySelector('.js-commit-link')).toBeNull();
vm.$el.querySelector('.js-commit-link'),
).toBeNull();
}); });
it('should render pipeline graph', () => { it('should render pipeline graph', () => {
expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length); expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(
mockData.pipeline.details.stages.length,
);
}); });
it('should render coverage information', () => { it('should render coverage information', () => {
expect( expect(vm.$el.querySelector('.media-body').textContent).toContain(
vm.$el.querySelector('.media-body').textContent, `Coverage ${mockData.pipeline.coverage}`,
).toContain(`Coverage ${mockData.pipeline.coverage}`); );
}); });
}); });
...@@ -172,9 +174,7 @@ describe('MRWidgetPipeline', () => { ...@@ -172,9 +174,7 @@ describe('MRWidgetPipeline', () => {
troubleshootingDocsPath: 'help', troubleshootingDocsPath: 'help',
}); });
expect( expect(vm.$el.querySelector('.media-body').textContent).not.toContain('Coverage');
vm.$el.querySelector('.media-body').textContent,
).not.toContain('Coverage');
}); });
}); });
......
...@@ -218,6 +218,7 @@ export default { ...@@ -218,6 +218,7 @@ export default {
diverged_commits_count: 0, diverged_commits_count: 0,
only_allow_merge_if_pipeline_succeeds: false, only_allow_merge_if_pipeline_succeeds: false,
commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content', commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content',
merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', merge_commit_path:
troubleshooting_docs_path: 'help' 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help',
}; };
...@@ -454,7 +454,7 @@ describe('mrWidgetOptions', () => { ...@@ -454,7 +454,7 @@ describe('mrWidgetOptions', () => {
deployed_at: '2017-03-22T22:44:42.258Z', deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm', deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes, changes,
status: 'success' status: 'success',
}; };
beforeEach(done => { beforeEach(done => {
...@@ -607,33 +607,36 @@ describe('mrWidgetOptions', () => { ...@@ -607,33 +607,36 @@ describe('mrWidgetOptions', () => {
describe('with post merge deployments', () => { describe('with post merge deployments', () => {
beforeEach(done => { beforeEach(done => {
vm.mr.postMergeDeployments = [{ vm.mr.postMergeDeployments = [
id: 15, {
name: 'review/diplo', id: 15,
url: '/root/acets-review-apps/environments/15', name: 'review/diplo',
stop_url: '/root/acets-review-apps/environments/15/stop', url: '/root/acets-review-apps/environments/15',
metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', stop_url: '/root/acets-review-apps/environments/15/stop',
metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
external_url: 'http://diplo.', metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
external_url_formatted: 'diplo.', external_url: 'http://diplo.',
deployed_at: '2017-03-22T22:44:42.258Z', external_url_formatted: 'diplo.',
deployed_at_formatted: 'Mar 22, 2017 10:44pm', deployed_at: '2017-03-22T22:44:42.258Z',
changes: [ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
{ changes: [
path: 'index.html', {
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', path: 'index.html',
}, external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
{ },
path: 'imgs/gallery.html', {
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', path: 'imgs/gallery.html',
}, external_url:
{ 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
path: 'about/', },
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', {
}, path: 'about/',
], external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
status: 'success' },
}]; ],
status: 'success',
},
];
vm.$nextTick(done); vm.$nextTick(done);
}); });
......
...@@ -12,7 +12,7 @@ describe('collapsedGroupedDatePicker', () => { ...@@ -12,7 +12,7 @@ describe('collapsedGroupedDatePicker', () => {
}); });
describe('toggleCollapse events', () => { describe('toggleCollapse events', () => {
beforeEach((done) => { beforeEach(done => {
spyOn(vm, 'toggleSidebar'); spyOn(vm, 'toggleSidebar');
vm.minDate = new Date('07/17/2016'); vm.minDate = new Date('07/17/2016');
Vue.nextTick(done); Vue.nextTick(done);
...@@ -26,7 +26,7 @@ describe('collapsedGroupedDatePicker', () => { ...@@ -26,7 +26,7 @@ describe('collapsedGroupedDatePicker', () => {
}); });
describe('minDate and maxDate', () => { describe('minDate and maxDate', () => {
beforeEach((done) => { beforeEach(done => {
vm.minDate = new Date('07/17/2016'); vm.minDate = new Date('07/17/2016');
vm.maxDate = new Date('07/17/2017'); vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done); Vue.nextTick(done);
...@@ -42,7 +42,7 @@ describe('collapsedGroupedDatePicker', () => { ...@@ -42,7 +42,7 @@ describe('collapsedGroupedDatePicker', () => {
}); });
describe('minDate', () => { describe('minDate', () => {
beforeEach((done) => { beforeEach(done => {
vm.minDate = new Date('07/17/2016'); vm.minDate = new Date('07/17/2016');
Vue.nextTick(done); Vue.nextTick(done);
}); });
...@@ -56,7 +56,7 @@ describe('collapsedGroupedDatePicker', () => { ...@@ -56,7 +56,7 @@ describe('collapsedGroupedDatePicker', () => {
}); });
describe('maxDate', () => { describe('maxDate', () => {
beforeEach((done) => { beforeEach(done => {
vm.maxDate = new Date('07/17/2017'); vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done); Vue.nextTick(done);
}); });
......
...@@ -41,7 +41,7 @@ describe('sidebarDatePicker', () => { ...@@ -41,7 +41,7 @@ describe('sidebarDatePicker', () => {
expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None'); expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None');
}); });
it('should render date-picker when editing', (done) => { it('should render date-picker when editing', done => {
vm.editing = true; vm.editing = true;
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.pika-label')).toBeDefined(); expect(vm.$el.querySelector('.pika-label')).toBeDefined();
...@@ -50,7 +50,7 @@ describe('sidebarDatePicker', () => { ...@@ -50,7 +50,7 @@ describe('sidebarDatePicker', () => {
}); });
describe('editable', () => { describe('editable', () => {
beforeEach((done) => { beforeEach(done => {
vm.editable = true; vm.editable = true;
Vue.nextTick(done); Vue.nextTick(done);
}); });
...@@ -59,7 +59,7 @@ describe('sidebarDatePicker', () => { ...@@ -59,7 +59,7 @@ describe('sidebarDatePicker', () => {
expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit'); expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit');
}); });
it('should enable editing when edit button is clicked', (done) => { it('should enable editing when edit button is clicked', done => {
vm.isLoading = false; vm.isLoading = false;
Vue.nextTick(() => { Vue.nextTick(() => {
vm.$el.querySelector('.title .btn-blank').click(); vm.$el.querySelector('.title .btn-blank').click();
...@@ -70,7 +70,7 @@ describe('sidebarDatePicker', () => { ...@@ -70,7 +70,7 @@ describe('sidebarDatePicker', () => {
}); });
}); });
it('should render date if selectedDate', (done) => { it('should render date if selectedDate', done => {
vm.selectedDate = new Date('07/07/2017'); vm.selectedDate = new Date('07/07/2017');
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017'); expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017');
...@@ -79,7 +79,7 @@ describe('sidebarDatePicker', () => { ...@@ -79,7 +79,7 @@ describe('sidebarDatePicker', () => {
}); });
describe('selectedDate and editable', () => { describe('selectedDate and editable', () => {
beforeEach((done) => { beforeEach(done => {
vm.selectedDate = new Date('07/07/2017'); vm.selectedDate = new Date('07/07/2017');
vm.editable = true; vm.editable = true;
Vue.nextTick(done); Vue.nextTick(done);
...@@ -100,7 +100,7 @@ describe('sidebarDatePicker', () => { ...@@ -100,7 +100,7 @@ describe('sidebarDatePicker', () => {
}); });
describe('showToggleSidebar', () => { describe('showToggleSidebar', () => {
beforeEach((done) => { beforeEach(done => {
vm.showToggleSidebar = true; vm.showToggleSidebar = true;
Vue.nextTick(done); Vue.nextTick(done);
}); });
......
...@@ -49,7 +49,9 @@ describe('DropdownValueCollapsedComponent', () => { ...@@ -49,7 +49,9 @@ describe('DropdownValueCollapsedComponent', () => {
const vmMoreLabels = createComponent(mockMoreLabels); const vmMoreLabels = createComponent(mockMoreLabels);
expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more'); expect(vmMoreLabels.labelsList).toBe(
'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more',
);
vmMoreLabels.$destroy(); vmMoreLabels.$destroy();
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment