Commit fdbdd45d authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into fe-commit-mr-pipelines

* master: (65 commits)
  Fixed eslint test failure
  Fixed adding to list bug
  Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index
  Fixed modal lists dropdown not updating when list is deleted
  Fixed remove btn error after creating new issue in list
  Removed duplicated test
  Removed Masonry, instead uses groups of data
  Uses mixins for repeated functions
  Fixed up specs
  Props use objects with required & type values
  Removes labels instead of closing issue when clicking remove button
  Fixed JS lint errors
  Fixed issue card spec
  Added webkit CSS properties
  Fixed bug with empty state showing after search Fixed users href path being incorrect
  Fixed bug where 2 un-selected issues would stay on selected tab
  Fixed DB schema Changed how components are added in objects
  Added remove button
  Add optional id property to the issue schema
  Fixed issue link href
  ...
parents 183772c4 d76b9291
......@@ -222,7 +222,6 @@ gem 'chronic_duration', '~> 0.10.6'
gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
......
......@@ -266,8 +266,6 @@ GEM
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab-markup (1.5.1)
gitlab-turbolinks-classic (2.5.6)
coffee-rails
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
......@@ -891,7 +889,6 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
gitlab-turbolinks-classic (~> 2.5, >= 2.5.6)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
/* global Turbolinks */
(function() {
this.Admin = (function() {
......@@ -42,10 +41,10 @@
return $('.change-owner-link').show();
});
$('li.project_member').bind('ajax:success', function() {
return Turbolinks.visit(location.href);
return gl.utils.refreshCurrentPage();
});
$('li.group_member').bind('ajax:success', function() {
return Turbolinks.visit(location.href);
return gl.utils.refreshCurrentPage();
});
showBlacklistType = function() {
if ($("input[name='blacklist_type']:checked").val() === 'file') {
......
......@@ -24,9 +24,7 @@
/*= require jquery.waitforimages */
/*= require jquery.atwho */
/*= require jquery.scrollTo */
/*= require jquery.turbolinks */
/*= require js.cookie */
/*= require turbolinks */
/*= require autosave */
/*= require bootstrap/affix */
/*= require bootstrap/alert */
......@@ -64,7 +62,7 @@
/*= require es6-promise.auto */
(function () {
document.addEventListener('page:fetch', function () {
document.addEventListener('beforeunload', function () {
// Unbind scroll events
$(document).off('scroll');
// Close any open tooltips
......
......@@ -14,10 +14,12 @@
//= require ./components/board_sidebar
//= require ./components/new_list_dropdown
//= require vue_shared/vue_resource_interceptor
//= require ./components/modal/index
$(() => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore;
window.gl = window.gl || {};
......@@ -31,7 +33,8 @@ $(() => {
el: $boardApp,
components: {
'board': gl.issueBoards.Board,
'board-sidebar': gl.issueBoards.BoardSidebar
'board-sidebar': gl.issueBoards.BoardSidebar,
'board-add-issues-modal': gl.issueBoards.IssuesModal,
},
data: {
state: Store.state,
......@@ -40,6 +43,8 @@ $(() => {
boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase,
rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: Store.detail
},
computed: {
......@@ -48,7 +53,7 @@ $(() => {
},
},
created () {
gl.boardService = new BoardService(this.endpoint, this.boardId);
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
},
mounted () {
Store.disabled = this.disabled;
......@@ -59,8 +64,6 @@ $(() => {
if (list.type === 'done') {
list.position = Infinity;
} else if (list.type === 'backlog') {
list.position = -1;
}
});
......@@ -81,4 +84,27 @@ $(() => {
gl.issueBoards.newListDropdownInit();
}
});
gl.IssueBoardsModalAddBtn = new Vue({
mixins: [gl.issueBoards.ModalMixins],
el: '#js-add-issues-btn',
data: {
modal: ModalStore.store,
store: Store.state,
},
computed: {
disabled() {
return Store.shouldAddBlankState();
},
},
template: `
<button
class="btn btn-create pull-right prepend-left-10 has-tooltip"
type="button"
:disabled="disabled"
@click="toggleModal(true)">
Add issues
</button>
`,
});
});
......@@ -22,7 +22,8 @@
props: {
list: Object,
disabled: Boolean,
issueLinkBase: String
issueLinkBase: String,
rootPath: String,
},
data () {
return {
......
/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
//= require ./issue_card_inner
/* global Vue */
(() => {
......@@ -9,12 +10,16 @@
gl.issueBoards.BoardCard = Vue.extend({
template: '#js-board-list-card',
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
props: {
list: Object,
issue: Object,
issueLinkBase: String,
disabled: Boolean,
index: Number
index: Number,
rootPath: String,
},
data () {
return {
......@@ -28,31 +33,6 @@
}
},
methods: {
filterByLabel (label, e) {
let labelToggleText = label.title;
const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
$(e.target).tooltip('hide');
if (labelIndex === -1) {
Store.state.filters['label_name'].push(label.title);
$('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
} else {
Store.state.filters['label_name'].splice(labelIndex, 1);
labelToggleText = Store.state.filters['label_name'][0];
$(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
}
const selectedLabels = Store.state.filters['label_name'];
if (selectedLabels.length === 0) {
labelToggleText = 'Label';
} else if (selectedLabels.length > 1) {
labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
}
$('.labels-filter .dropdown-toggle-text').text(labelToggleText);
Store.updateFiltersUrl();
},
mouseDown () {
this.showDetail = true;
},
......@@ -71,6 +51,7 @@
Store.detail.issue = {};
} else {
Store.detail.issue = this.issue;
Store.detail.list = this.list;
}
}
}
......
......@@ -23,6 +23,7 @@
issues: Array,
loading: Boolean,
issueLinkBase: String,
rootPath: String,
},
data () {
return {
......
......@@ -37,6 +37,7 @@
$(this.$refs.submitButton).enable();
Store.detail.issue = issue;
Store.detail.list = this.list;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
......
......@@ -4,6 +4,7 @@
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
//= require ./sidebar/remove_issue
(() => {
const Store = gl.issueBoards.BoardsStore;
......@@ -18,7 +19,8 @@
data() {
return {
detail: Store.detail,
issue: {}
issue: {},
list: {},
};
},
computed: {
......@@ -36,6 +38,7 @@
}
this.issue = this.detail.issue;
this.list = this.detail.list;
},
deep: true
},
......@@ -60,6 +63,9 @@
new LabelsSelect();
new Sidebar();
gl.Subscription.bindAll('.subscription');
}
},
components: {
removeBtn: gl.issueBoards.RemoveIssueBtn,
},
});
})();
/* global Vue */
(() => {
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.IssueCardInner = Vue.extend({
props: {
issue: {
type: Object,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
list: {
type: Object,
required: false,
},
rootPath: {
type: String,
required: true,
},
},
methods: {
showLabel(label) {
if (!this.list) return true;
return !this.list.label || label.id !== this.list.label.id;
},
filterByLabel(label, e) {
let labelToggleText = label.title;
const labelIndex = Store.state.filters.label_name.indexOf(label.title);
$(e.currentTarget).tooltip('hide');
if (labelIndex === -1) {
Store.state.filters.label_name.push(label.title);
$('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
} else {
Store.state.filters.label_name.splice(labelIndex, 1);
labelToggleText = Store.state.filters.label_name[0];
$(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
}
const selectedLabels = Store.state.filters.label_name;
if (selectedLabels.length === 0) {
labelToggleText = 'Label';
} else if (selectedLabels.length > 1) {
labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
}
$('.labels-filter .dropdown-toggle-text').text(labelToggleText);
Store.updateFiltersUrl();
},
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
},
template: `
<div>
<h4 class="card-title">
<i
class="fa fa-eye-slash confidential-icon"
v-if="issue.confidential"></i>
<a
:href="issueLinkBase + '/' + issue.id"
:title="issue.title">
{{ issue.title }}
</a>
</h4>
<div class="card-footer">
<span
class="card-number"
v-if="issue.id">
#{{ issue.id }}
</span>
<a
class="card-assignee has-tooltip"
:href="rootPath + issue.assignee.username"
:title="'Assigned to ' + issue.assignee.name"
v-if="issue.assignee"
data-container="body">
<img
class="avatar avatar-inline s20"
:src="issue.assignee.avatar"
width="20"
height="20"
:alt="'Avatar for ' + issue.assignee.name" />
</a>
<button
class="label color-label has-tooltip"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
@click="filterByLabel(label, $event)"
:style="labelStyle(label)"
:title="label.description"
data-container="body">
{{ label.title }}
</button>
</div>
</div>
`,
});
})();
/* global Vue */
(() => {
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalEmptyState = Vue.extend({
mixins: [gl.issueBoards.ModalMixins],
data() {
return ModalStore.store;
},
props: {
image: {
type: String,
required: true,
},
newIssuePath: {
type: String,
required: true,
},
},
computed: {
contents() {
const obj = {
title: 'You haven\'t added any issues to your project yet',
content: `
An issue can be a bug, a todo or a feature request that needs to be
discussed in a project. Besides, issues are searchable and filterable.
`,
};
if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet';
obj.content = `
Go back to <strong>All issues</strong> and select some issues
to add to your board.
`;
}
return obj;
},
},
template: `
<section class="empty-state">
<div class="row">
<div class="col-xs-12 col-sm-6 col-sm-push-6">
<aside class="svg-content" v-html="image"></aside>
</div>
<div class="col-xs-12 col-sm-6 col-sm-pull-6">
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
<a
:href="newIssuePath"
class="btn btn-success btn-inverted"
v-if="activeTab === 'all'">
New issue
</a>
<button
type="button"
class="btn btn-default"
@click="changeTab('all')"
v-if="activeTab === 'selected'">
All issues
</button>
</div>
</div>
</div>
</section>
`,
});
})();
/* eslint-disable no-new */
//= require ./lists_dropdown
/* global Vue */
/* global Flash */
(() => {
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalFooter = Vue.extend({
mixins: [gl.issueBoards.ModalMixins],
data() {
return {
modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state,
};
},
computed: {
submitDisabled() {
return !ModalStore.selectedCount();
},
submitText() {
const count = ModalStore.selectedCount();
return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
},
},
methods: {
addIssues() {
const list = this.modal.selectedList || this.state.lists[0];
const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.globalId);
// Post the data to the backend
gl.boardService.bulkUpdate(issueIds, {
add_label_ids: [list.label.id],
}).catch(() => {
new Flash('Failed to update issues, please try again.', 'alert');
selectedIssues.forEach((issue) => {
list.removeIssue(issue);
list.issuesSize -= 1;
});
});
// Add the issues on the frontend
selectedIssues.forEach((issue) => {
list.addIssue(issue);
list.issuesSize += 1;
});
this.toggleModal(false);
},
},
components: {
'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
},
template: `
<footer
class="form-actions add-issues-footer">
<div class="pull-left">
<button
class="btn btn-success"
type="button"
:disabled="submitDisabled"
@click="addIssues">
{{ submitText }}
</button>
<span class="inline add-issues-footer-to-list">
to list
</span>
<lists-dropdown></lists-dropdown>
</div>
<button
class="btn btn-default pull-right"
type="button"
@click="toggleModal(false)">
Cancel
</button>
</footer>
`,
});
})();
/* global Vue */
//= require ./tabs
(() => {
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalHeader = Vue.extend({
mixins: [gl.issueBoards.ModalMixins],
data() {
return ModalStore.store;
},
computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return 'Select all';
}
return 'Deselect all';
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
},
methods: {
toggleAll() {
this.$refs.selectAllBtn.blur();
ModalStore.toggleAll();
},
},
components: {
'modal-tabs': gl.issueBoards.ModalTabs,
},
template: `
<div>
<header class="add-issues-header form-actions">
<h2>
Add issues
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
@click="toggleModal(false)">
<span aria-hidden="true">×</span>
</button>
</h2>
</header>
<modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
<div
class="add-issues-search append-bottom-10"
v-if="showSearch">
<input
placeholder="Search issues..."
class="form-control"
type="search"
v-model="searchTerm" />
<button
type="button"
class="btn btn-success btn-inverted prepend-left-10"
ref="selectAllBtn"
@click="toggleAll">
{{ selectAllText }}
</button>
</div>
</div>
`,
});
})();
/* global Vue */
/* global ListIssue */
//= require ./header
//= require ./list
//= require ./footer
//= require ./empty_state
(() => {
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.IssuesModal = Vue.extend({
props: {
blankStateImage: {
type: String,
required: true,
},
newIssuePath: {
type: String,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
watch: {
page() {
this.loadIssues();
},
searchTerm() {
this.searchOperation();
},
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
this.loadIssues()
.then(() => {
this.loading = false;
});
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
}
},
},
methods: {
searchOperation: _.debounce(function searchOperationDebounce() {
this.loadIssues(true);
}, 500),
loadIssues(clearIssues = false) {
return gl.boardService.getBacklog({
search: this.searchTerm,
page: this.page,
per: this.perPage,
}).then((res) => {
const data = res.json();
if (clearIssues) {
this.issues = [];
}
data.issues.forEach((issueObj) => {
const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
issue.selected = !!foundSelectedIssue;
this.issues.push(issue);
});
this.loadingNewPage = false;
if (!this.issuesCount) {
this.issuesCount = data.size;
}
});
},
},
computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0;
},
showEmptyState() {
if (!this.loading && this.issuesCount === 0) {
return true;
}
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
},
components: {
'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList,
'modal-footer': gl.issueBoards.ModalFooter,
'empty-state': gl.issueBoards.ModalEmptyState,
},
template: `
<div
class="add-issues-modal"
v-if="showAddIssuesModal">
<div class="add-issues-container">
<modal-header></modal-header>
<modal-list
:issue-link-base="issueLinkBase"
:root-path="rootPath"
v-if="!loading && showList"></modal-list>
<empty-state
v-if="showEmptyState"
:image="blankStateImage"
:new-issue-path="newIssuePath"></empty-state>
<section
class="add-issues-list text-center"
v-if="loading">
<div class="add-issues-list-loading">
<i class="fa fa-spinner fa-spin"></i>
</div>
</section>
<modal-footer></modal-footer>
</div>
</div>
`,
});
})();
/* global Vue */
/* global ListIssue */
/* global bp */
(() => {
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalList = Vue.extend({
props: {
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
watch: {
activeTab() {
if (this.activeTab === 'all') {
ModalStore.purgeUnselectedIssues();
}
},
},
computed: {
loopIssues() {
if (this.activeTab === 'all') {
return this.issues;
}
return this.selectedIssues;
},
groupedIssues() {
const groups = [];
this.loopIssues.forEach((issue, i) => {
const index = i % this.columns;
if (!groups[index]) {
groups.push([]);
}
groups[index].push(issue);
});
return groups;
},
},
methods: {
scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage);
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
&& currentPage === this.page) {
this.loadingNewPage = true;
this.page += 1;
}
},
toggleIssue(e, issue) {
if (e.target.tagName !== 'A') {
ModalStore.toggleIssue(issue);
}
},
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
showIssue(issue) {
if (this.activeTab === 'all') return true;
const index = ModalStore.selectedIssueIndex(issue);
return index !== -1;
},
setColumnCount() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'lg' || breakpoint === 'md') {
this.columns = 3;
} else if (breakpoint === 'sm') {
this.columns = 2;
} else {
this.columns = 1;
}
},
},
mounted() {
this.scrollHandlerWrapper = this.scrollHandler.bind(this);
this.setColumnCountWrapper = this.setColumnCount.bind(this);
this.setColumnCount();
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
window.addEventListener('resize', this.setColumnCountWrapper);
},
beforeDestroy() {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
window.removeEventListener('resize', this.setColumnCountWrapper);
},
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
template: `
<section
class="add-issues-list add-issues-list-columns"
ref="list">
<div
v-for="group in groupedIssues"
class="add-issues-list-column">
<div
v-for="issue in group"
v-if="showIssue(issue)"
class="card-parent">
<div
class="card"
:class="{ 'is-active': issue.selected }"
@click="toggleIssue($event, issue)">
<issue-card-inner
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath">
</issue-card-inner>
<span
:aria-label="'Issue #' + issue.id + ' selected'"
aria-checked="true"
v-if="issue.selected"
class="issue-card-selected text-center">
<i class="fa fa-check"></i>
</span>
</div>
</div>
</div>
</section>
`,
});
})();
/* global Vue */
(() => {
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
data() {
return {
modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state,
};
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[0];
},
},
destroyed() {
this.modal.selectedList = null;
},
template: `
<div class="dropdown inline">
<button
class="dropdown-menu-toggle"
type="button"
data-toggle="dropdown"
aria-expanded="false">
<span
class="dropdown-label-box"
:style="{ backgroundColor: selected.label.color }">
</span>
{{ selected.title }}
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
<li
v-for="list in state.lists"
v-if="list.type == 'label'">
<a
href="#"
role="button"
:class="{ 'is-active': list.id == selected.id }"
@click.prevent="modal.selectedList = list">
<span
class="dropdown-label-box"
:style="{ backgroundColor: list.label.color }">
</span>
{{ list.title }}
</a>
</li>
</ul>
</div>
</div>
`,
});
})();
/* global Vue */
(() => {
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalTabs = Vue.extend({
mixins: [gl.issueBoards.ModalMixins],
data() {
return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
},
},
destroyed() {
this.activeTab = 'all';
},
template: `
<div class="top-area prepend-top-10 append-bottom-10">
<ul class="nav-links issues-state-filters">
<li :class="{ 'active': activeTab == 'all' }">
<a
href="#"
role="button"
@click.prevent="changeTab('all')">
All issues
<span class="badge">
{{ issuesCount }}
</span>
</a>
</li>
<li :class="{ 'active': activeTab == 'selected' }">
<a
href="#"
role="button"
@click.prevent="changeTab('selected')">
Selected issues
<span class="badge">
{{ selectedCount }}
</span>
</a>
</li>
</ul>
</div>
`,
});
})();
/* eslint-disable no-new */
/* global Vue */
/* global Flash */
(() => {
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.RemoveIssueBtn = Vue.extend({
props: {
issue: {
type: Object,
required: true,
},
list: {
type: Object,
required: true,
},
},
methods: {
removeIssue() {
const issue = this.issue;
const lists = issue.getLists();
const labelIds = lists.map(list => list.label.id);
// Post the remove data
gl.boardService.bulkUpdate([issue.globalId], {
remove_label_ids: labelIds,
}).catch(() => {
new Flash('Failed to remove issue from board, please try again.', 'alert');
lists.forEach((list) => {
list.addIssue(issue);
});
});
// Remove from the frontend store
lists.forEach((list) => {
list.removeIssue(issue);
});
Store.detail.issue = {};
},
},
template: `
<div
class="block list"
v-if="list.type !== 'done'">
<button
class="btn btn-default btn-block"
type="button"
@click="removeIssue">
Remove from board
</button>
</div>
`,
});
})();
(() => {
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalMixins = {
methods: {
toggleModal(toggle) {
ModalStore.store.showAddIssuesModal = toggle;
},
changeTab(tab) {
ModalStore.store.activeTab = tab;
},
},
};
})();
......@@ -6,12 +6,15 @@
class ListIssue {
constructor (obj) {
this.globalId = obj.id;
this.id = obj.iid;
this.title = obj.title;
this.confidential = obj.confidential;
this.dueDate = obj.due_date;
this.subscribed = obj.subscribed;
this.labels = [];
this.selected = false;
this.assignee = false;
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee);
......
......@@ -9,7 +9,7 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1;
this.preset = ['done', 'blank'].indexOf(this.type) > -1;
this.filters = gl.issueBoards.BoardsStore.state.filters;
this.page = 1;
this.loading = true;
......
......@@ -2,7 +2,13 @@
/* global Vue */
class BoardService {
constructor (root, boardId) {
constructor (root, bulkUpdatePath, boardId) {
this.boards = Vue.resource(`${root}{/id}.json`, {}, {
issues: {
method: 'GET',
url: `${root}/${boardId}/issues.json`
}
});
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
......@@ -10,7 +16,12 @@ class BoardService {
}
});
this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {});
this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, {
bulkUpdate: {
method: 'POST',
url: bulkUpdatePath,
},
});
Vue.http.interceptors.push((request, next) => {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
......@@ -65,6 +76,20 @@ class BoardService {
issue
});
}
getBacklog(data) {
return this.boards.issues(data);
}
bulkUpdate(issueIds, extraData = {}) {
const data = {
update: Object.assign(extraData, {
issuable_ids: issueIds.join(','),
}),
};
return this.issues.bulkUpdate(data);
}
}
window.BoardService = BoardService;
......@@ -34,15 +34,10 @@
},
new (listObj) {
const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
list
.save()
.then(() => {
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
this.state.lists = _.sortBy(this.state.lists, 'position');
});
this.removeBlankState();
......@@ -52,7 +47,7 @@
},
shouldAddBlankState () {
// Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]);
return !(this.state.lists.filter(list => list.type !== 'done')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
......@@ -102,7 +97,7 @@
listTo.addIssue(issue, listFrom, newIndex);
}
if (listTo.type === 'done' && listFrom.type !== 'backlog') {
if (listTo.type === 'done') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
......
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
class ModalStore {
constructor() {
this.store = {
columns: 3,
issues: [],
issuesCount: false,
selectedIssues: [],
showAddIssuesModal: false,
activeTab: 'all',
selectedList: null,
searchTerm: '',
loading: false,
loadingNewPage: false,
page: 1,
perPage: 50,
};
}
selectedCount() {
return this.getSelectedIssues().length;
}
toggleIssue(issueObj) {
const issue = issueObj;
const selected = issue.selected;
issue.selected = !selected;
if (!selected) {
this.addSelectedIssue(issue);
} else {
this.removeSelectedIssue(issue);
}
}
toggleAll() {
const select = this.selectedCount() !== this.store.issues.length;
this.store.issues.forEach((issue) => {
const issueUpdate = issue;
if (issueUpdate.selected !== select) {
issueUpdate.selected = select;
if (select) {
this.addSelectedIssue(issue);
} else {
this.removeSelectedIssue(issue);
}
}
});
}
getSelectedIssues() {
return this.store.selectedIssues.filter(issue => issue.selected);
}
addSelectedIssue(issue) {
const index = this.selectedIssueIndex(issue);
if (index === -1) {
this.store.selectedIssues.push(issue);
}
}
removeSelectedIssue(issue, forcePurge = false) {
if (this.store.activeTab === 'all' || forcePurge) {
this.store.selectedIssues = this.store.selectedIssues
.filter(fIssue => fIssue.id !== issue.id);
}
}
purgeUnselectedIssues() {
this.store.selectedIssues.forEach((issue) => {
if (!issue.selected) {
this.removeSelectedIssue(issue, true);
}
});
}
selectedIssueIndex(issue) {
return this.store.selectedIssues.indexOf(issue);
}
findSelectedIssue(issue) {
return this.store.selectedIssues
.filter(filteredIssue => filteredIssue.id === issue.id)[0];
}
}
gl.issueBoards.ModalStore = new ModalStore();
})();
......@@ -43,6 +43,7 @@
BreakpointInstance.prototype.getBreakpointSize = function() {
var $visibleDevice;
$visibleDevice = this.visibleDevice;
// TODO: Consider refactoring in light of turbolinks removal.
// the page refreshed via turbolinks
if (!$visibleDevice().length) {
this.setup();
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
/* global Breakpoints */
/* global Turbolinks */
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
......@@ -127,7 +126,7 @@
pageUrl += DOWN_BUILD_TRACE;
}
return Turbolinks.visit(pageUrl);
return gl.utils.visitUrl(pageUrl);
}
};
})(this)
......
......@@ -4,6 +4,7 @@
(() => {
const UNFOLD_COUNT = 20;
let isBound = false;
class Diff {
constructor() {
......@@ -17,10 +18,12 @@
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
if (!isBound) {
$(document)
.off('click', '.js-unfold, .diff-line-num a')
.on('click', '.js-unfold', this.handleClickUnfold.bind(this))
.on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
isBound = true;
}
this.openAnchoredDiff();
}
......
......@@ -9,7 +9,7 @@
this.setupMapping();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('page:fetch', this.cleanupWrapper);
document.addEventListener('beforeunload', this.cleanupWrapper);
}
cleanup() {
......@@ -20,7 +20,7 @@
this.setupMapping();
document.removeEventListener('page:fetch', this.cleanupWrapper);
document.removeEventListener('beforeunload', this.cleanupWrapper);
}
setupMapping() {
......
/* global Turbolinks */
(() => {
class FilteredSearchManager {
constructor() {
......@@ -15,13 +13,13 @@
this.dropdownManager.setDropdown();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('page:fetch', this.cleanupWrapper);
document.addEventListener('beforeunload', this.cleanupWrapper);
}
}
cleanup() {
this.unbindEvents();
document.removeEventListener('page:fetch', this.cleanupWrapper);
document.removeEventListener('beforeunload', this.cleanupWrapper);
}
bindEvents() {
......@@ -200,7 +198,9 @@
paths.push(`search=${sanitized}`);
}
Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`);
const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`;
gl.utils.visitUrl(parameterizedUrl);
}
getUsernameParams() {
......
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
/* global Turbolinks */
(function() {
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
......@@ -723,7 +722,7 @@
if ($el.length) {
var href = $el.attr('href');
if (href && href !== '#') {
Turbolinks.visit(href);
gl.utils.visitUrl(href);
} else {
$el.first().trigger('click');
}
......
/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
/* global Issuable */
/* global Turbolinks */
((global) => {
var issuable_created;
......@@ -119,7 +118,7 @@
issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
issuesUrl += formData;
return Turbolinks.visit(issuesUrl);
return gl.utils.visitUrl(issuesUrl);
};
})(this),
initResetFilters: function() {
......@@ -130,7 +129,7 @@
const baseIssuesUrl = target.href;
$form.attr('action', baseIssuesUrl);
Turbolinks.visit(baseIssuesUrl);
gl.utils.visitUrl(baseIssuesUrl);
});
},
initChecks: function() {
......
......@@ -95,7 +95,6 @@
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
history.replaceState({
turbolinks: true,
url: newState,
}, document.title, newState);
return newState;
......
......@@ -161,6 +161,9 @@
gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
};
gl.text.pluralize = function(str, count) {
return str + (count > 1 || count === 0 ? 's' : '');
};
return gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
......
......@@ -76,5 +76,11 @@
hashIndex = url.indexOf('#');
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
};
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
w.gl.utils.visitUrl = (url) => {
document.location.href = url;
};
})(window);
}).call(this);
......@@ -171,7 +171,6 @@
// This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({
turbolinks: false,
url: value
// We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
/* global Turbolinks */
(function() {
Turbolinks.enableProgressBar();
$(document).on('page:fetch', function() {
window.addEventListener('beforeunload', function() {
$('.tanuki-logo').addClass('animate');
});
$(document).on('page:change', function() {
$('.tanuki-logo').removeClass('animate');
});
}).call(this);
......@@ -179,12 +179,13 @@
// Ensure parameters and hash come along for the ride
newState += location.search + location.hash;
// TODO: Consider refactoring in light of turbolinks removal.
// Replace the current history state with the new one without breaking
// Turbolinks' history.
//
// See https://github.com/rails/turbolinks/issues/363
window.history.replaceState({
turbolinks: true,
url: newState,
}, document.title, newState);
......
......@@ -2,7 +2,6 @@
/* global notify */
/* global notifyPermissions */
/* global merge_request_widget */
/* global Turbolinks */
((global) => {
var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
......@@ -69,13 +68,13 @@
}
MergeRequestWidget.prototype.clearEventListeners = function() {
return $(document).off('page:change.merge_request');
return $(document).off('DOMContentLoaded');
};
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
allowedPages = ['show', 'commits', 'pipelines', 'changes'];
$(document).on('page:change.merge_request', (function(_this) {
$(document).on('DOMContentLoaded', (function(_this) {
return function() {
var page;
page = $('body').data('page').split(':').last();
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global Cookies */
/* global Turbolinks */
/* global ProjectSelect */
(function() {
......@@ -58,8 +57,8 @@
};
Project.prototype.initRefSwitcher = function() {
var refListItem = document.createElement('li'),
refLink = document.createElement('a');
var refListItem = document.createElement('li');
var refLink = document.createElement('a');
refLink.href = '#';
......@@ -118,7 +117,7 @@
var $form = $dropdown.closest('form');
var action = $form.attr('action');
var divider = action.indexOf('?') < 0 ? '?' : '&';
Turbolinks.visit(action + '' + divider + '' + $form.serialize());
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
}
}
});
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
/* global Turbolinks */
(function() {
this.ProjectImport = (function() {
function ProjectImport() {
setTimeout(function() {
return Turbolinks.visit(location.href);
return gl.utils.visitUrl(location.href);
}, 5000);
}
......
......@@ -9,7 +9,7 @@
this.find('.js-render-math').renderMath();
};
$(document).on('ready page:load', function() {
$(document).on('ready load', function() {
return $('body').renderGFM();
});
}).call(this);
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
/* global Turbolinks */
/* global findFileURL */
(function() {
......@@ -23,7 +22,7 @@
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
if (typeof findFileURL !== "undefined" && findFileURL !== null) {
Mousetrap.bind('t', function() {
return Turbolinks.visit(findFileURL);
return gl.utils.visitUrl(findFileURL);
});
}
}
......
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */
/* global Mousetrap */
/* global Turbolinks */
/* global ShortcutsNavigation */
/* global sidebar */
......@@ -80,7 +79,7 @@
ShortcutsIssuable.prototype.editIssue = function() {
var $editBtn;
$editBtn = $('.issuable-edit');
return Turbolinks.visit($editBtn.attr('href'));
return gl.utils.visitUrl($editBtn.attr('href'));
};
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
......
......@@ -40,7 +40,7 @@
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
.on('click', 'html, body', (e) => this.handleClickEvent(e))
.on('page:change', () => this.renderState())
.on('DOMContentLoaded', () => this.renderState())
.on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState();
}
......
......@@ -89,7 +89,7 @@
destroy() {
this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('page:before-unload');
$(document).off('visibilitychange').off('beforeunload');
}
/* private */
......@@ -111,8 +111,9 @@
}
initPageUnloadHandling() {
// TODO: Consider refactoring in light of turbolinks removal.
// prevent interval continuing after page change, when kept in cache by Turbolinks
$(document).on('page:before-unload', () => this.cancel());
$(document).on('beforeunload', () => this.cancel());
}
handleVisibilityChange(e) {
......
/* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */
/* global UsersSelect */
/* global Turbolinks */
((global) => {
class Todos {
......@@ -34,7 +33,7 @@
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
gl.utils.visitUrl(this.action + '&' + $(this).serialize());
});
}
......@@ -142,7 +141,7 @@
};
url = gl.utils.mergeUrlParams(pageParams, url);
}
return Turbolinks.visit(url);
return gl.utils.visitUrl(url);
}
}
......@@ -156,7 +155,7 @@
e.preventDefault();
return window.open(todoLink, '_blank');
} else {
return Turbolinks.visit(todoLink);
return gl.utils.visitUrl(todoLink);
}
}
}
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */
/* global Turbolinks */
(function() {
this.TreeView = (function() {
function TreeView() {
......@@ -15,7 +15,7 @@
e.preventDefault();
return window.open(path, '_blank');
} else {
return Turbolinks.visit(path);
return gl.utils.visitUrl(path);
}
}
});
......@@ -57,7 +57,7 @@
} else if (e.which === 13) {
path = $('.tree-item.selected .tree-item-file-name a').attr('href');
if (path) {
return Turbolinks.visit(path);
return gl.utils.visitUrl(path);
}
}
});
......
......@@ -149,7 +149,6 @@ content on the Users#show page.
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
url: new_state
}, document.title, new_state);
return new_state;
......
/* global Vue, Turbolinks, gl */
/* global Vue, gl */
/* eslint-disable no-param-reassign */
//= require vue_shared/components/table_pagination
......@@ -35,7 +35,7 @@
},
methods: {
change(pagenum, apiScope) {
Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
},
},
template: `
......
......@@ -7,12 +7,12 @@
window.removeEventListener('beforeunload', removeIntervals);
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
document.removeEventListener('page:fetch', removeAll);
document.removeEventListener('beforeunload', removeAll);
};
window.addEventListener('beforeunload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
document.addEventListener('page:fetch', removeAll);
document.addEventListener('beforeunload', removeAll);
};
})(window.gl || (window.gl = {}));
......@@ -13,6 +13,8 @@
gl.VueGlPagination = Vue.extend({
props: {
// TODO: Consider refactoring in light of turbolinks removal.
/**
This function will take the information given by the pagination component
And make a new Turbolinks call
......@@ -20,7 +22,7 @@
Here is an example `change` method:
change(pagenum, apiScope) {
Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
},
*/
......
......@@ -31,7 +31,6 @@
@import "framework/modal.scss";
@import "framework/nav.scss";
@import "framework/pagination.scss";
@import "framework/progress.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
@import "framework/sidebar.scss";
......
......@@ -227,6 +227,11 @@
}
}
.dropdown-menu-drop-up {
top: auto;
bottom: 100%;
}
.dropdown-menu-large {
width: 340px;
}
......
html.turbolinks-progress-bar::before {
background-color: $progress-color!important;
height: 2px!important;
box-shadow: 0 0 10px $progress-color, 0 0 5px $progress-color;
}
......@@ -250,7 +250,7 @@
}
.issue-boards-search {
width: 290px;
width: 395px;
.form-control {
display: inline-block;
......@@ -354,3 +354,135 @@
padding-right: 0;
}
}
.add-issues-modal {
display: -webkit-flex;
display: flex;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba($black, .3);
z-index: 9999;
}
.add-issues-container {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
width: 90vw;
height: 85vh;
max-width: 1100px;
min-height: 500px;
margin: auto;
padding: 25px 15px 0;
background-color: $white-light;
border-radius: $border-radius-default;
box-shadow: 0 2px 12px rgba($black, .5);
.empty-state {
display: -webkit-flex;
display: flex;
-webkit-flex: 1;
flex: 1;
margin-top: 0;
> .row {
width: 100%;
margin: auto 0;
}
.svg-content {
margin-top: -40px;
}
}
}
.add-issues-header {
margin: -25px -15px -5px;
border-top: 0;
border-bottom: 1px solid $border-color;
border-top-right-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
> h2 {
margin: 0;
font-size: 18px;
}
}
.add-issues-search {
display: -webkit-flex;
display: flex;
}
.add-issues-list-column {
width: 100%;
@media (min-width: $screen-sm-min) {
width: 50%;
}
@media (min-width: $screen-md-min) {
width: (100% / 3);
}
}
.add-issues-list {
display: -webkit-flex;
display: flex;
-webkit-flex: 1;
flex: 1;
padding-top: 3px;
margin-left: -$gl-vert-padding;
margin-right: -$gl-vert-padding;
overflow-y: scroll;
.card-parent {
padding: 0 5px 5px;
}
.card {
border: 1px solid $border-gray-dark;
box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, .3);
cursor: pointer;
}
}
.add-issues-list-loading {
-webkit-align-self: center;
align-self: center;
width: 100%;
padding-left: $gl-vert-padding;
padding-right: $gl-vert-padding;
font-size: 35px;
}
.add-issues-footer {
margin: auto -15px 0;
padding-left: 15px;
padding-right: 15px;
border-bottom-right-radius: $border-radius-default;
border-bottom-left-radius: $border-radius-default;
}
.add-issues-footer-to-list {
padding-left: $gl-vert-padding;
padding-right: $gl-vert-padding;
line-height: 34px;
}
.issue-card-selected {
position: absolute;
right: -3px;
top: -3px;
width: 17px;
background-color: $blue-light;
color: $white-light;
border: 1px solid $border-blue-light;
font-size: 9px;
line-height: 15px;
border-radius: 50%;
}
......@@ -10,10 +10,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@last_push = current_user.recent_push
respond_to do |format|
format.html
format.html { @last_push = current_user.recent_push }
format.atom do
event_filter
load_events
......
......@@ -7,7 +7,7 @@ module Projects
def index
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
issues = issues.page(params[:page])
issues = issues.page(params[:page]).per(params[:per] || 20)
render json: {
issues: serialize_as_json(issues),
......@@ -59,7 +59,7 @@ module Projects
end
def filter_params
params.merge(board_id: params[:board_id], id: params[:list_id])
params.merge(board_id: params[:board_id], id: params[:list_id]).compact
end
def move_params
......@@ -73,7 +73,7 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
labels: true,
only: [:iid, :title, :confidential, :due_date],
only: [:id, :iid, :title, :confidential, :due_date],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
......
......@@ -6,7 +6,9 @@ module BoardsHelper
endpoint: namespace_project_boards_path(@project.namespace, @project),
board_id: board.id,
disabled: "#{!can?(current_user, :admin_list, @project)}",
issue_link_base: namespace_project_issues_path(@project.namespace, @project)
issue_link_base: namespace_project_issues_path(@project.namespace, @project),
root_path: root_path,
bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project),
}
end
end
module JavascriptHelper
def page_specific_javascript_tag(js)
javascript_include_tag asset_path(js), { "data-turbolinks-track" => true }
javascript_include_tag asset_path(js)
end
end
......@@ -5,10 +5,6 @@ class Board < ActiveRecord::Base
validates :project, presence: true
def backlog_list
lists.merge(List.backlog).take
end
def done_list
lists.merge(List.done).take
end
......
......@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
enum list_type: { backlog: 0, label: 1, done: 2 }
enum list_type: { label: 1, done: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
......
......@@ -12,7 +12,6 @@ module Boards
def create_board!
board = project.boards.create
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
board
......
......@@ -3,8 +3,8 @@ module Boards
class ListService < BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless list.movable?
issues = with_list_label(issues) if list.movable?
issues = without_board_labels(issues) unless movable_list?
issues = with_list_label(issues) if movable_list?
issues
end
......@@ -15,7 +15,13 @@ module Boards
end
def list
@list ||= board.lists.find(params[:id])
return @list if defined?(@list)
@list = board.lists.find(params[:id]) if params.key?(:id)
end
def movable_list?
@movable_list ||= list.present? && list.movable?
end
def filter_params
......@@ -40,7 +46,7 @@ module Boards
end
def set_state
params[:state] = list.done? ? 'closed' : 'opened'
params[:state] = list && list.done? ? 'closed' : 'opened'
end
def board_label_ids
......
......@@ -304,6 +304,18 @@ module SlashCommands
params '@user'
command :cc
desc 'Defines target branch for MR'
params '<Local branch name>'
condition do
issuable.respond_to?(:target_branch) &&
(current_user.can?(:"update_#{issuable.to_ability_name}", issuable) ||
issuable.new_record?)
end
command :target_branch do |target_branch_param|
branch_name = target_branch_param.strip
@updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
end
def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
......
......@@ -6,4 +6,4 @@
- providers.each do |provider|
%span.light
- has_icon = provider_has_icon?(provider)
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn')
......@@ -3,7 +3,7 @@
.col-md-4.col-lg-6
= users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true)
.help-block.append-bottom-10
Search for users by name, username, or email, or invite new ones using their email address.
Search for members by name, username, or email, or invite new ones using their email address.
.col-md-3.col-lg-2
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
......@@ -16,7 +16,7 @@
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
.help-block.append-bottom-10
On this date, the user(s) will automatically lose access to this group and all of its projects.
On this date, the member(s) will automatically lose access to this group and all of its projects.
.col-md-2
= f.submit 'Add to group', class: "btn btn-create btn-block"
......@@ -7,7 +7,7 @@
- if can?(current_user, :admin_group_member, @group)
.project-members-new.append-bottom-default
%p.clearfix
Add new user to
Add new member to
%strong= @group.name
= render "new_group_member"
......@@ -15,7 +15,7 @@
.append-bottom-default.clearfix
%h5.member.existing-title
Existing users
Existing members
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
......@@ -24,7 +24,7 @@
= render 'shared/members/sort_dropdown'
.panel.panel-default
.panel-heading
Users with access to
Members with access to
%strong= @group.name
%span.badge= @members.total_count
%ul.content-list
......
......@@ -82,7 +82,7 @@
rather than Git. Please convert
= link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview'
and go through the
= link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true'
= link_to 'import flow', status_import_bitbucket_path
again.
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
......@@ -33,6 +33,8 @@
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
= yield :project_javascripts
= csrf_meta_tags
- unless browser.safari?
......
......@@ -4,9 +4,6 @@
%body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= Gon::Base.render_data
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
= yield :scripts_body_top
= render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav
......
......@@ -3,7 +3,7 @@
- header_title project_title(@project) unless header_title
- nav "project"
- content_for :scripts_body_top do
- content_for :project_javascripts do
- project = @target_project || @project
- if @project_wiki && @page
- preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug)
......
......@@ -82,7 +82,7 @@
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect
- else
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do
Connect
%hr
- if current_user.can_change_username?
......
......@@ -24,5 +24,10 @@
":list" => "list",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
":key" => "_uid" }
= render "projects/boards/components/sidebar"
%board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'),
"new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project),
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath" }
......@@ -29,6 +29,7 @@
":loading" => "list.loading",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
"ref" => "board-list" }
- if can?(current_user, :admin_list, @project)
= render "projects/boards/components/blank_state"
......@@ -34,6 +34,7 @@
":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
":disabled" => "disabled",
":key" => "issue.id" }
%li.board-list-count.text-center{ "v-if" => "showCount" }
......
......@@ -4,25 +4,7 @@
"@mousedown" => "mouseDown",
"@mousemove" => "mouseMove",
"@mouseup" => "showIssue($event)" }
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => 'issueLinkBase + "/" + issue.id',
":title" => "issue.title" }
{{ issue.title }}
.card-footer
%span.card-number{ "v-if" => "issue.id" }
= precede '#' do
{{ issue.id }}
%a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username",
":title" => '"Assigned to " + issue.assignee.name',
"v-if" => "issue.assignee",
data: { container: 'body' } }
%img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20, alt: "Avatar" }
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
type: "button",
"v-if" => "(!list.label || label.id !== list.label.id)",
"@click" => "filterByLabel(label, $event)",
":style" => "{ backgroundColor: label.color, color: label.textColor }",
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
%issue-card-inner{ ":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath" }
......@@ -22,3 +22,5 @@
= render "projects/boards/components/sidebar/due_date"
= render "projects/boards/components/sidebar/labels"
= render "projects/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue",
":list" => "list" }
......@@ -8,8 +8,12 @@
- last_line = right.new_pos if right
%tr.line_holder.parallel
- if left
- if left.meta?
- case left.type
- when 'match'
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel
- when 'nonewline'
%td.old_line.diff-line-num
%td.line_content.match= left.text
- else
- left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left)
......@@ -21,8 +25,12 @@
%td.line_content.parallel
- if right
- if right.meta?
- case right.type
- when 'match'
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel
- when 'nonewline'
%td.new_line.diff-line-num
%td.line_content.match= right.text
- else
- right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right)
......
......@@ -109,10 +109,10 @@
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title
:javascript
var merge_request;
merge_request = new MergeRequest({
$(function () {
new MergeRequest({
action: "#{controller.action_name}"
});
});
var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}";
......@@ -50,7 +50,7 @@
= icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
......
......@@ -38,8 +38,9 @@
#js-boards-search.issue-boards-search
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
#js-add-issues-btn.pull-right.prepend-left-10
.dropdown.pull-right
%button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
%button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
......@@ -91,5 +92,5 @@
new SubscriptionSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
gl.utils.visitUrl(this.action + '&' + $(this).serialize());
});
---
title: Replace word user with member
merge_request: 8872
author:
---
title: Remove turbolinks.
merge_request: !8570
author:
---
title: Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index
merge_request: 8956
author:
---
title: Adds /target_branch slash command functionality for merge requests
merge_request:
author: YarNayar
......@@ -267,7 +267,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :boards, only: [:index, :show] do
scope module: :boards do
resources :issues, only: [:update]
resources :issues, only: [:index, :update]
resources :lists, only: [:index, :create, :update, :destroy] do
collection do
......
class RemoveBacklogListsFromBoards < ActiveRecord::Migration
DOWNTIME = false
def up
execute <<-SQL
DELETE FROM lists WHERE list_type = 0;
SQL
end
def down
execute <<-SQL
INSERT INTO lists (board_id, list_type, created_at, updated_at)
SELECT boards.id, 0, NOW(), NOW()
FROM boards;
SQL
end
end
......@@ -34,3 +34,4 @@ do.
| `/remove_estimate` | Remove estimated time |
| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or substract spent time |
| `/remove_time_spent` | Remove time spent |
| `/target_branch <Branch Name>` | Set target branch for current merge request |
......@@ -501,6 +501,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I fill in merge request search with "Fe"' do
fill_in 'issuable_search', with: "Fe"
page.within '.merge-requests-holder' do
find('.merge-request')
end
end
step 'I click the "Target branch" dropdown' do
......
......@@ -37,7 +37,7 @@ module API
end
desc 'Get the lists of a project board' do
detail 'Does not include `backlog` and `done` lists. This feature was introduced in 8.13'
detail 'Does not include `done` list. This feature was introduced in 8.13'
success Entities::List
end
get '/lists' do
......
......@@ -18,9 +18,19 @@ describe Projects::Boards::IssuesController do
end
describe 'GET index' do
let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
context 'with invalid board id' do
it 'returns a not found 404 response' do
list_issues user: user, board: 999, list: list2
expect(response).to have_http_status(404)
end
end
context 'when list id is present' do
context 'with valid list id' do
it 'returns issues that have the list label applied' do
johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
issue = create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
......@@ -36,19 +46,29 @@ describe Projects::Boards::IssuesController do
end
end
context 'with invalid board id' do
context 'with invalid list id' do
it 'returns a not found 404 response' do
list_issues user: user, board: 999, list: list2
list_issues user: user, board: board, list: 999
expect(response).to have_http_status(404)
end
end
end
context 'with invalid list id' do
it 'returns a not found 404 response' do
list_issues user: user, board: board, list: 999
context 'when list id is missing' do
it 'returns opened issues without board labels applied' do
bug = create(:label, project: project, name: 'Bug')
create(:issue, project: project)
create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [development])
create(:labeled_issue, project: project, labels: [bug])
expect(response).to have_http_status(404)
list_issues user: user, board: board
parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('issues')
expect(parsed_response.length).to eq 2
end
end
......@@ -65,13 +85,17 @@ describe Projects::Boards::IssuesController do
end
end
def list_issues(user:, board:, list:)
def list_issues(user:, board:, list: nil)
sign_in(user)
get :index, namespace_id: project.namespace.to_param,
params = {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
board_id: board.to_param,
list_id: list.to_param
list_id: list.try(:to_param)
}
get :index, params.compact
end
end
......
......@@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do
parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('lists')
expect(parsed_response.length).to eq 3
expect(parsed_response.length).to eq 2
end
context 'with unauthorized user' do
......
......@@ -3,7 +3,6 @@ FactoryGirl.define do
project factory: :empty_project
after(:create) do |board|
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
end
end
......
......@@ -3,6 +3,18 @@ FactoryGirl.define do
project factory: :empty_project
author factory: :user
trait(:created) { action Event::CREATED }
trait(:updated) { action Event::UPDATED }
trait(:closed) { action Event::CLOSED }
trait(:reopened) { action Event::REOPENED }
trait(:pushed) { action Event::PUSHED }
trait(:commented) { action Event::COMMENTED }
trait(:merged) { action Event::MERGED }
trait(:joined) { action Event::JOINED }
trait(:left) { action Event::LEFT }
trait(:destroyed) { action Event::DESTROYED }
trait(:expired) { action Event::EXPIRED }
factory :closed_issue_event do
action { Event::CLOSED }
target factory: :closed_issue
......
......@@ -6,12 +6,6 @@ FactoryGirl.define do
sequence(:position)
end
factory :backlog_list, parent: :list do
list_type :backlog
label nil
position nil
end
factory :done_list, parent: :list do
list_type :done
label nil
......
require 'rails_helper'
describe 'Issue Boards add issue modal', :feature, :js do
include WaitForAjax
include WaitForVueResource
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let!(:planning) { create(:label, project: project, name: 'Planning') }
let!(:label) { create(:label, project: project) }
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: label, position: 1) }
let!(:issue) { create(:issue, project: project) }
let!(:issue2) { create(:issue, project: project) }
before do
project.team << [user, :master]
login_as(user)
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
end
context 'modal interaction' do
it 'opens modal' do
click_button('Add issues')
expect(page).to have_selector('.add-issues-modal')
end
it 'closes modal' do
click_button('Add issues')
page.within('.add-issues-modal') do
find('.close').click
end
expect(page).not_to have_selector('.add-issues-modal')
end
it 'closes modal if cancel button clicked' do
click_button('Add issues')
page.within('.add-issues-modal') do
click_button 'Cancel'
end
expect(page).not_to have_selector('.add-issues-modal')
end
end
context 'issues list' do
before do
click_button('Add issues')
wait_for_vue_resource
end
it 'loads issues' do
page.within('.add-issues-modal') do
page.within('.nav-links') do
expect(page).to have_content('2')
end
expect(page).to have_selector('.card', count: 2)
end
end
it 'shows selected issues' do
page.within('.add-issues-modal') do
click_link 'Selected issues'
expect(page).not_to have_selector('.card')
end
end
context 'list dropdown' do
it 'resets after deleting list' do
page.within('.add-issues-modal') do
expect(find('.add-issues-footer')).to have_button(planning.title)
click_button 'Cancel'
end
first('.board-delete').click
click_button('Add issues')
wait_for_vue_resource
page.within('.add-issues-modal') do
expect(find('.add-issues-footer')).not_to have_button(planning.title)
expect(find('.add-issues-footer')).to have_button(label.title)
end
end
end
context 'search' do
it 'returns issues' do
page.within('.add-issues-modal') do
find('.form-control').native.send_keys(issue.title)
expect(page).to have_selector('.card', count: 1)
end
end
it 'returns no issues' do
page.within('.add-issues-modal') do
find('.form-control').native.send_keys('testing search')
expect(page).not_to have_selector('.card')
expect(page).not_to have_content("You haven't added any issues to your project yet")
end
end
end
context 'selecing issues' do
it 'selects single issue' do
page.within('.add-issues-modal') do
first('.card').click
page.within('.nav-links') do
expect(page).to have_content('Selected issues 1')
end
end
end
it 'changes button text' do
page.within('.add-issues-modal') do
first('.card').click
expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue')
end
end
it 'changes button text with plural' do
page.within('.add-issues-modal') do
all('.card').each do |el|
el.click
end
expect(first('.add-issues-footer .btn')).to have_content('Add 2 issues')
end
end
it 'shows only selected issues on selected tab' do
page.within('.add-issues-modal') do
first('.card').click
click_link 'Selected issues'
expect(page).to have_selector('.card', count: 1)
end
end
it 'selects all issues' do
page.within('.add-issues-modal') do
click_button 'Select all'
expect(page).to have_selector('.is-active', count: 2)
end
end
it 'deselects all issues' do
page.within('.add-issues-modal') do
click_button 'Select all'
expect(page).to have_selector('.is-active', count: 2)
click_button 'Deselect all'
expect(page).not_to have_selector('.is-active')
end
end
it 'selects all that arent already selected' do
page.within('.add-issues-modal') do
first('.card').click
expect(page).to have_selector('.is-active', count: 1)
click_button 'Select all'
expect(page).to have_selector('.is-active', count: 2)
end
end
it 'unselects from selected tab' do
page.within('.add-issues-modal') do
first('.card').click
click_link 'Selected issues'
first('.card').click
expect(page).not_to have_selector('.is-active')
end
end
end
context 'adding issues' do
it 'adds to board' do
page.within('.add-issues-modal') do
first('.card').click
click_button 'Add 1 issue'
end
page.within(first('.board')) do
expect(page).to have_selector('.card')
end
end
it 'adds to second list' do
page.within('.add-issues-modal') do
first('.card').click
click_button planning.title
click_link label.title
click_button 'Add 1 issue'
end
page.within(find('.board:nth-child(2)')) do
expect(page).to have_selector('.card')
end
end
end
end
end
......@@ -20,7 +20,7 @@ describe 'Issue Boards', feature: true, js: true do
before do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
expect(page).to have_selector('.board', count: 2)
end
it 'shows blank state' do
......@@ -31,18 +31,18 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.board-blank-state')) do
click_button("Nevermind, I'll use my own")
end
expect(page).to have_selector('.board', count: 2)
expect(page).to have_selector('.board', count: 1)
end
it 'creates default lists' do
lists = ['Backlog', 'To Do', 'Doing', 'Done']
lists = ['To Do', 'Doing', 'Done']
page.within(find('.board-blank-state')) do
click_button('Add default lists')
end
wait_for_vue_resource
expect(page).to have_selector('.board', count: 4)
expect(page).to have_selector('.board', count: 3)
page.all('.board').each_with_index do |list, i|
expect(list.find('.board-title')).to have_content(lists[i])
......@@ -64,42 +64,41 @@ describe 'Issue Boards', feature: true, js: true do
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
let!(:issue1) { create(:issue, project: project, assignee: user) }
let!(:issue2) { create(:issue, project: project, author: user2) }
let!(:issue3) { create(:issue, project: project) }
let!(:issue4) { create(:issue, project: project) }
let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) }
let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) }
let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) }
let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) }
let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) }
let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
let!(:issue8) { create(:closed_issue, project: project) }
let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) }
before do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 4)
expect(page).to have_selector('.board', count: 3)
expect(find('.board:nth-child(1)')).to have_selector('.card')
expect(find('.board:nth-child(2)')).to have_selector('.card')
expect(find('.board:nth-child(3)')).to have_selector('.card')
expect(find('.board:nth-child(4)')).to have_selector('.card')
end
it 'shows lists' do
expect(page).to have_selector('.board', count: 4)
expect(page).to have_selector('.board', count: 3)
end
it 'shows description tooltip on list title' do
page.within('.board:nth-child(2)') do
page.within('.board:nth-child(1)') do
expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')
end
end
it 'shows issues in lists' do
wait_for_board_cards(1, 8)
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
end
it 'shows confidential issues with icon' do
......@@ -108,19 +107,6 @@ describe 'Issue Boards', feature: true, js: true do
end
end
it 'search backlog list' do
page.within('#js-boards-search') do
find('.form-control').set(issue1.title)
end
wait_for_vue_resource
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
end
it 'search done list' do
page.within('#js-boards-search') do
find('.form-control').set(issue8.title)
......@@ -130,8 +116,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
end
it 'search list' do
......@@ -141,157 +126,135 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
end
it 'allows user to delete board' do
page.within(find('.board:nth-child(2)')) do
page.within(find('.board:nth-child(1)')) do
find('.board-delete').click
end
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
expect(page).to have_selector('.board', count: 2)
end
it 'removes checkmark in new list dropdown after deleting' do
click_button 'Add list'
wait_for_ajax
page.within(find('.board:nth-child(2)')) do
page.within(find('.board:nth-child(1)')) do
find('.board-delete').click
end
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active')
expect(page).to have_selector('.board', count: 2)
end
it 'infinite scrolls list' do
50.times do
create(:issue, project: project)
create(:labeled_issue, project: project, labels: [planning])
end
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
page.within(find('.board', match: :first)) do
expect(page.find('.board-header')).to have_content('56')
expect(page.find('.board-header')).to have_content('58')
expect(page).to have_selector('.card', count: 20)
expect(page).to have_content('Showing 20 of 56 issues')
expect(page).to have_content('Showing 20 of 58 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource
expect(page).to have_selector('.card', count: 40)
expect(page).to have_content('Showing 40 of 56 issues')
expect(page).to have_content('Showing 40 of 58 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource
expect(page).to have_selector('.card', count: 56)
expect(page).to have_selector('.card', count: 58)
expect(page).to have_content('Showing all issues')
end
end
context 'backlog' do
it 'shows issues in backlog with no labels' do
wait_for_board_cards(1, 6)
end
it 'moves issue from backlog into list' do
drag_to(list_to_index: 1)
wait_for_vue_resource
wait_for_board_cards(1, 5)
wait_for_board_cards(2, 3)
end
end
context 'done' do
it 'shows list of done issues' do
wait_for_board_cards(4, 1)
wait_for_board_cards(3, 1)
wait_for_ajax
end
it 'moves issue to done' do
drag_to(list_from_index: 0, list_to_index: 3)
drag_to(list_from_index: 0, list_to_index: 2)
wait_for_board_cards(1, 5)
wait_for_board_cards(1, 7)
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
wait_for_board_cards(4, 2)
expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
expect(find('.board:nth-child(4)')).to have_content(issue9.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2)
expect(find('.board:nth-child(3)')).to have_content(issue9.title)
expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end
it 'removes all of the same issue to done' do
drag_to(list_from_index: 1, list_to_index: 3)
drag_to(list_from_index: 0, list_to_index: 2)
wait_for_board_cards(1, 6)
wait_for_board_cards(2, 1)
wait_for_board_cards(3, 1)
wait_for_board_cards(4, 2)
wait_for_board_cards(1, 7)
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
expect(find('.board:nth-child(2)')).not_to have_content(issue6.title)
expect(find('.board:nth-child(4)')).to have_content(issue6.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
expect(find('.board:nth-child(3)')).to have_content(issue9.title)
expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end
end
context 'lists' do
it 'changes position of list' do
drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
drag_to(list_from_index: 1, list_to_index: 0, selector: '.board-header')
wait_for_board_cards(1, 6)
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
wait_for_board_cards(4, 1)
wait_for_board_cards(1, 2)
wait_for_board_cards(2, 8)
wait_for_board_cards(3, 1)
expect(find('.board:nth-child(2)')).to have_content(development.title)
expect(find('.board:nth-child(2)')).to have_content(planning.title)
expect(find('.board:nth-child(1)')).to have_content(development.title)
expect(find('.board:nth-child(1)')).to have_content(planning.title)
end
it 'issue moves between lists' do
drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
drag_to(list_from_index: 0, card_index: 1, list_to_index: 1)
wait_for_board_cards(1, 6)
wait_for_board_cards(2, 1)
wait_for_board_cards(3, 3)
wait_for_board_cards(4, 1)
wait_for_board_cards(1, 7)
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 1)
expect(find('.board:nth-child(3)')).to have_content(issue6.title)
expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
expect(find('.board:nth-child(2)')).to have_content(issue6.title)
expect(find('.board:nth-child(2)').all('.card').last).not_to have_content(development.title)
end
it 'issue moves between lists' do
drag_to(list_from_index: 2, list_to_index: 1)
drag_to(list_from_index: 1, list_to_index: 0)
wait_for_board_cards(1, 6)
wait_for_board_cards(2, 3)
wait_for_board_cards(1, 9)
wait_for_board_cards(2, 1)
wait_for_board_cards(3, 1)
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(issue7.title)
expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
expect(find('.board:nth-child(1)')).to have_content(issue7.title)
expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title)
end
it 'issue moves from done' do
drag_to(list_from_index: 3, list_to_index: 1)
drag_to(list_from_index: 2, list_to_index: 1)
expect(find('.board:nth-child(2)')).to have_content(issue8.title)
wait_for_board_cards(1, 6)
wait_for_board_cards(1, 8)
wait_for_board_cards(2, 3)
wait_for_board_cards(3, 2)
wait_for_board_cards(4, 0)
wait_for_board_cards(3, 0)
end
context 'issue card' do
......@@ -324,7 +287,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
expect(page).to have_selector('.board', count: 5)
expect(page).to have_selector('.board', count: 4)
end
it 'creates new list for Backlog label' do
......@@ -337,7 +300,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
expect(page).to have_selector('.board', count: 5)
expect(page).to have_selector('.board', count: 4)
end
it 'creates new list for Done label' do
......@@ -350,7 +313,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
expect(page).to have_selector('.board', count: 5)
expect(page).to have_selector('.board', count: 4)
end
it 'keeps dropdown open after adding new list' do
......@@ -366,21 +329,6 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.issue-boards-search')).to have_selector('.open')
end
it 'moves issues from backlog into new list' do
wait_for_board_cards(1, 6)
click_button 'Add list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
click_link testing.title
end
wait_for_vue_resource
wait_for_board_cards(1, 5)
end
it 'creates new list from a new label' do
click_button 'Add list'
......@@ -397,7 +345,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
wait_for_vue_resource
expect(page).to have_selector('.board', count: 5)
expect(page).to have_selector('.board', count: 4)
end
end
end
......@@ -418,7 +366,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
wait_for_empty_boards((2..4))
wait_for_empty_boards((2..3))
end
it 'filters by assignee' do
......@@ -437,7 +385,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
wait_for_empty_boards((2..4))
wait_for_empty_boards((2..3))
end
it 'filters by milestone' do
......@@ -454,10 +402,9 @@ describe 'Issue Boards', feature: true, js: true do
end
wait_for_vue_resource
wait_for_board_cards(1, 0)
wait_for_board_cards(2, 1)
wait_for_board_cards(1, 1)
wait_for_board_cards(2, 0)
wait_for_board_cards(3, 0)
wait_for_board_cards(4, 0)
end
it 'filters by label' do
......@@ -474,7 +421,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
wait_for_empty_boards((2..4))
wait_for_empty_boards((2..3))
end
it 'filters by label with space after reload' do
......@@ -530,7 +477,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'infinite scrolls list with label filter' do
50.times do
create(:labeled_issue, project: project, labels: [testing])
create(:labeled_issue, project: project, labels: [planning, testing])
end
page.within '.issues-filters' do
......@@ -580,32 +527,12 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
wait_for_empty_boards((2..4))
end
it 'filters by no label' do
page.within '.issues-filters' do
click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do
click_link("No Label")
wait_for_vue_resource
find('.dropdown-menu-close').click
end
end
wait_for_vue_resource
wait_for_board_cards(1, 5)
wait_for_board_cards(2, 0)
wait_for_board_cards(3, 0)
wait_for_board_cards(4, 1)
wait_for_empty_boards((2..3))
end
it 'filters by clicking label button on issue' do
page.within(find('.board', match: :first)) do
expect(page).to have_selector('.card', count: 6)
expect(page).to have_selector('.card', count: 8)
expect(find('.card', match: :first)).to have_content(bug.title)
click_button(bug.title)
wait_for_vue_resource
......@@ -614,7 +541,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
wait_for_empty_boards((2..4))
wait_for_empty_boards((2..3))
page.within('.labels-filter') do
expect(find('.dropdown-toggle-text')).to have_content(bug.title)
......
......@@ -6,6 +6,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, position: 0) }
let(:user) { create(:user) }
context 'authorized user' do
......@@ -17,7 +18,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
expect(page).to have_selector('.board', count: 2)
end
it 'displays new issue button' do
......@@ -25,7 +26,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
end
it 'does not display new issue button in done list' do
page.within('.board:nth-child(3)') do
page.within('.board:nth-child(2)') do
expect(page).not_to have_selector('.board-issue-count-holder .btn')
end
end
......
......@@ -4,14 +4,17 @@ describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let!(:label) { create(:label, project: project) }
let!(:label2) { create(:label, project: project) }
let(:project) { create(:empty_project, :public) }
let!(:milestone) { create(:milestone, project: project) }
let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) }
let!(:issue) { create(:issue, project: project) }
let!(:development) { create(:label, project: project, name: 'Development') }
let!(:bug) { create(:label, project: project, name: 'Bug') }
let!(:regression) { create(:label, project: project, name: 'Regression') }
let!(:stretch) { create(:label, project: project, name: 'Stretch') }
let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
before do
project.team << [user, :master]
......@@ -62,8 +65,22 @@ describe 'Issue Boards', feature: true, js: true do
end
page.within('.issue-boards-sidebar') do
expect(page).to have_content(issue.title)
expect(page).to have_content(issue.to_reference)
expect(page).to have_content(issue2.title)
expect(page).to have_content(issue2.to_reference)
end
end
it 'removes card from board when clicking remove button' do
page.within(first('.board')) do
first('.card').click
end
page.within('.issue-boards-sidebar') do
click_button 'Remove from board'
end
page.within(first('.board')) do
expect(page).to have_selector('.card', count: 1)
end
end
......@@ -244,22 +261,22 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
click_link label.title
click_link bug.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
expect(page).to have_selector('.label', count: 1)
expect(page).to have_content(label.title)
expect(page).to have_selector('.label', count: 3)
expect(page).to have_content(bug.title)
end
end
page.within(first('.board')) do
page.within(first('.card')) do
expect(page).to have_selector('.label', count: 1)
expect(page).to have_content(label.title)
expect(page).to have_selector('.label', count: 2)
expect(page).to have_content(bug.title)
end
end
end
......@@ -274,32 +291,32 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
click_link label.title
click_link label2.title
click_link bug.title
click_link regression.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
expect(page).to have_selector('.label', count: 2)
expect(page).to have_content(label.title)
expect(page).to have_content(label2.title)
expect(page).to have_selector('.label', count: 4)
expect(page).to have_content(bug.title)
expect(page).to have_content(regression.title)
end
end
page.within(first('.board')) do
page.within(first('.card')) do
expect(page).to have_selector('.label', count: 2)
expect(page).to have_content(label.title)
expect(page).to have_content(label2.title)
expect(page).to have_selector('.label', count: 3)
expect(page).to have_content(bug.title)
expect(page).to have_content(regression.title)
end
end
end
it 'removes a label' do
page.within(first('.board')) do
find('.card:nth-child(2)').click
first('.card').click
end
page.within('.labels') do
......@@ -307,22 +324,22 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
click_link label.title
click_link stretch.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
expect(page).to have_selector('.label', count: 0)
expect(page).not_to have_content(label.title)
expect(page).to have_selector('.label', count: 1)
expect(page).not_to have_content(stretch.title)
end
end
page.within(first('.board')) do
page.within(find('.card:nth-child(2)')) do
expect(page).not_to have_selector('.label', count: 1)
expect(page).not_to have_content(label.title)
page.within(first('.card')) do
expect(page).not_to have_selector('.label')
expect(page).not_to have_content(stretch.title)
end
end
end
......
......@@ -120,5 +120,79 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
expect(page).not_to have_content '/due 2016-08-28'
end
end
describe '/target_branch command in merge request' do
let(:another_project) { create(:project, :public) }
let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
before do
logout
another_project.team << [user, :master]
login_with(user)
end
it 'changes target_branch in new merge_request' do
visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts)
fill_in "merge_request_title", with: 'My brand new feature'
fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
click_button "Submit merge request"
merge_request = another_project.merge_requests.first
expect(merge_request.description).to eq "le feature \nFeature description:"
expect(merge_request.target_branch).to eq 'fix'
end
it 'does not change target branch when merge request is edited' do
new_merge_request = create(:merge_request, source_project: another_project)
visit edit_namespace_project_merge_request_path(another_project.namespace, another_project, new_merge_request)
fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n"
click_button "Save changes"
new_merge_request = another_project.merge_requests.first
expect(new_merge_request.description).to include('/target_branch')
expect(new_merge_request.target_branch).not_to eq('fix')
end
end
describe '/target_branch command from note' do
context 'when the current user can change target branch' do
it 'changes target branch from a note' do
write_note("message start \n/target_branch merge-test\n message end.")
expect(page).not_to have_content('/target_branch')
expect(page).to have_content('message start')
expect(page).to have_content('message end.')
expect(merge_request.reload.target_branch).to eq 'merge-test'
end
it 'does not fail when target branch does not exists' do
write_note('/target_branch totally_not_existing_branch')
expect(page).not_to have_content('/target_branch')
expect(merge_request.target_branch).to eq 'feature'
end
end
context 'when current user can not change target branch' do
let(:guest) { create(:user) }
before do
project.team << [guest, :guest]
logout
login_with(guest)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'does not change target branch' do
write_note('/target_branch merge-test')
expect(page).not_to have_content '/target_branch merge-test'
expect(merge_request.target_branch).to eq 'feature'
end
end
end
end
end
......@@ -10,15 +10,12 @@ describe ContributedProjectsFinder do
let!(:private_project) { create(:empty_project, :private) }
before do
private_project.team << [source_user, Gitlab::Access::MASTER]
private_project.team << [current_user, Gitlab::Access::DEVELOPER]
public_project.team << [source_user, Gitlab::Access::MASTER]
private_project.add_master(source_user)
private_project.add_developer(current_user)
public_project.add_master(source_user)
create(:event, action: Event::PUSHED, project: public_project,
target: public_project, author: source_user)
create(:event, action: Event::PUSHED, project: private_project,
target: private_project, author: source_user)
create(:event, :pushed, project: public_project, target: public_project, author: source_user)
create(:event, :pushed, project: private_project, target: private_project, author: source_user)
end
describe 'without a current user' do
......
......@@ -6,6 +6,7 @@
"confidential"
],
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"title": { "type": "string" },
"confidential": { "type": "boolean" },
......
......@@ -10,7 +10,7 @@
"id": { "type": "integer" },
"list_type": {
"type": "string",
"enum": ["backlog", "label", "done"]
"enum": ["label", "done"]
},
"label": {
"type": ["object", "null"],
......
......@@ -15,7 +15,7 @@
});
});
return load = function() {
return $(document).trigger('page:load');
return $(document).trigger('load');
};
});
}).call(this);
......@@ -34,11 +34,5 @@
$('#required5').val('1').change();
return expect($('.submit')).not.toBeDisabled();
});
return it('is called on page:load event', function() {
var spy;
spy = spyOn($.fn, 'requiresInput');
$(document).trigger('page:load');
return expect(spy).toHaveBeenCalled();
});
});
}).call(this);
......@@ -23,7 +23,7 @@
describe('Store', () => {
beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
Cookies.set('issue_board_welcome_hidden', 'false', {
......@@ -61,18 +61,6 @@ describe('Store', () => {
expect(list).toBeDefined();
});
it('finds list limited by type', () => {
gl.issueBoards.BoardsStore.addList({
id: 1,
position: 0,
title: 'Test',
list_type: 'backlog'
});
const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
expect(list).toBeDefined();
});
it('gets issue when new list added', (done) => {
gl.issueBoards.BoardsStore.addList(listObj);
const list = gl.issueBoards.BoardsStore.findList('id', 1);
......@@ -117,10 +105,7 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
});
it('check for blank state adding when backlog & done list exist', () => {
gl.issueBoards.BoardsStore.addList({
list_type: 'backlog'
});
it('check for blank state adding when done list exist', () => {
gl.issueBoards.BoardsStore.addList({
list_type: 'done'
});
......
/* global Vue */
/* global ListUser */
/* global ListLabel */
/* global listObj */
/* global ListIssue */
//= require jquery
//= require vue
//= require boards/models/issue
//= require boards/models/label
//= require boards/models/list
//= require boards/models/user
//= require boards/stores/boards_store
//= require boards/components/issue_card_inner
//= require ./mock_data
describe('Issue card component', () => {
const user = new ListUser({
id: 1,
name: 'testing 123',
username: 'test',
avatar: 'test_image',
});
const label1 = new ListLabel({
id: 3,
title: 'testing 123',
color: 'blue',
text_color: 'white',
description: 'test',
});
let component;
let issue;
let list;
beforeEach(() => {
setFixtures('<div class="test-container"></div>');
list = listObj;
issue = new ListIssue({
title: 'Testing',
iid: 1,
confidential: false,
labels: [list.label],
});
component = new Vue({
el: document.querySelector('.test-container'),
data() {
return {
list,
issue,
issueLinkBase: '/test',
rootPath: '/',
};
},
components: {
'issue-card': gl.issueBoards.IssueCardInner,
},
template: `
<issue-card
:issue="issue"
:list="list"
:issue-link-base="issueLinkBase"
:root-path="rootPath"></issue-card>
`,
});
});
it('renders issue title', () => {
expect(
component.$el.querySelector('.card-title').textContent,
).toContain(issue.title);
});
it('includes issue base in link', () => {
expect(
component.$el.querySelector('.card-title a').getAttribute('href'),
).toContain('/test');
});
it('includes issue title on link', () => {
expect(
component.$el.querySelector('.card-title a').getAttribute('title'),
).toBe(issue.title);
});
it('does not render confidential icon', () => {
expect(
component.$el.querySelector('.fa-eye-flash'),
).toBeNull();
});
it('renders confidential icon', (done) => {
component.issue.confidential = true;
setTimeout(() => {
expect(
component.$el.querySelector('.confidential-icon'),
).not.toBeNull();
done();
}, 0);
});
it('renders issue ID with #', () => {
expect(
component.$el.querySelector('.card-number').textContent,
).toContain(`#${issue.id}`);
});
describe('assignee', () => {
it('does not render assignee', () => {
expect(
component.$el.querySelector('.card-assignee'),
).toBeNull();
});
describe('exists', () => {
beforeEach((done) => {
component.issue.assignee = user;
setTimeout(() => {
done();
}, 0);
});
it('renders assignee', () => {
expect(
component.$el.querySelector('.card-assignee'),
).not.toBeNull();
});
it('sets title', () => {
expect(
component.$el.querySelector('.card-assignee').getAttribute('title'),
).toContain(`Assigned to ${user.name}`);
});
it('sets users path', () => {
expect(
component.$el.querySelector('.card-assignee').getAttribute('href'),
).toBe('/test');
});
it('renders avatar', () => {
expect(
component.$el.querySelector('.card-assignee img'),
).not.toBeNull();
});
});
});
describe('labels', () => {
it('does not render any', () => {
expect(
component.$el.querySelector('.label'),
).toBeNull();
});
describe('exists', () => {
beforeEach((done) => {
component.issue.addLabel(label1);
setTimeout(() => {
done();
}, 0);
});
it('does not render list label', () => {
expect(
component.$el.querySelectorAll('.label').length,
).toBe(1);
});
it('renders label', () => {
expect(
component.$el.querySelector('.label').textContent,
).toContain(label1.title);
});
it('sets label description as title', () => {
expect(
component.$el.querySelector('.label').getAttribute('title'),
).toContain(label1.description);
});
it('sets background color of button', () => {
expect(
component.$el.querySelector('.label').style.backgroundColor,
).toContain(label1.color);
});
});
});
});
......@@ -20,7 +20,7 @@ describe('Issue model', () => {
let issue;
beforeEach(() => {
gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
issue = new ListIssue({
......
......@@ -24,7 +24,7 @@ describe('List model', () => {
beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
list = new List(listObj);
......
/* global Vue */
/* global ListIssue */
//= require jquery
//= require vue
//= require boards/models/issue
//= require boards/models/label
//= require boards/models/list
//= require boards/models/user
//= require boards/stores/modal_store
describe('Modal store', () => {
let issue;
let issue2;
const Store = gl.issueBoards.ModalStore;
beforeEach(() => {
// Setup default state
Store.store.issues = [];
Store.store.selectedIssues = [];
issue = new ListIssue({
title: 'Testing',
iid: 1,
confidential: false,
labels: [],
});
issue2 = new ListIssue({
title: 'Testing',
iid: 2,
confidential: false,
labels: [],
});
Store.store.issues.push(issue);
Store.store.issues.push(issue2);
});
it('returns selected count', () => {
expect(Store.selectedCount()).toBe(0);
});
it('toggles the issue as selected', () => {
Store.toggleIssue(issue);
expect(issue.selected).toBe(true);
expect(Store.selectedCount()).toBe(1);
});
it('toggles the issue as un-selected', () => {
Store.toggleIssue(issue);
Store.toggleIssue(issue);
expect(issue.selected).toBe(false);
expect(Store.selectedCount()).toBe(0);
});
it('toggles all issues as selected', () => {
Store.toggleAll();
expect(issue.selected).toBe(true);
expect(issue2.selected).toBe(true);
expect(Store.selectedCount()).toBe(2);
});
it('toggles all issues as un-selected', () => {
Store.toggleAll();
Store.toggleAll();
expect(issue.selected).toBe(false);
expect(issue2.selected).toBe(false);
expect(Store.selectedCount()).toBe(0);
});
it('toggles all if a single issue is selected', () => {
Store.toggleIssue(issue);
Store.toggleAll();
expect(issue.selected).toBe(true);
expect(issue2.selected).toBe(true);
expect(Store.selectedCount()).toBe(2);
});
it('adds issue to selected array', () => {
issue.selected = true;
Store.addSelectedIssue(issue);
expect(Store.selectedCount()).toBe(1);
});
it('removes issue from selected array', () => {
Store.addSelectedIssue(issue);
Store.removeSelectedIssue(issue);
expect(Store.selectedCount()).toBe(0);
});
it('returns selected issue index if present', () => {
Store.toggleIssue(issue);
expect(Store.selectedIssueIndex(issue)).toBe(0);
});
it('returns -1 if issue is not selected', () => {
expect(Store.selectedIssueIndex(issue)).toBe(-1);
});
it('finds the selected issue', () => {
Store.toggleIssue(issue);
expect(Store.findSelectedIssue(issue)).toBe(issue);
});
it('does not find a selected issue', () => {
expect(Store.findSelectedIssue(issue)).toBe(undefined);
});
it('does not remove from selected issue if tab is not all', () => {
Store.store.activeTab = 'selected';
Store.toggleIssue(issue);
Store.toggleIssue(issue);
expect(Store.store.selectedIssues.length).toBe(1);
expect(Store.selectedCount()).toBe(0);
});
it('gets selected issue array with only selected issues', () => {
Store.toggleIssue(issue);
Store.toggleIssue(issue2);
Store.toggleIssue(issue2);
expect(Store.getSelectedIssues().length).toBe(1);
});
});
......@@ -50,7 +50,6 @@
secondTab.click();
expect(historySpy).toHaveBeenCalledWith({
turbolinks: true,
url: newState,
}, document.title, newState);
});
......
/* eslint-disable no-new */
/* global Build */
/* global Turbolinks */
//= require lib/utils/datetime_utility
//= require lib/utils/url_utility
//= require build
//= require breakpoints
//= require jquery.nicescroll
//= require turbolinks
describe('Build', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
......@@ -167,7 +166,7 @@ describe('Build', () => {
});
it('reloads the page when the build is done', () => {
spyOn(Turbolinks, 'visit');
spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
const [{ success, context }] = $.ajax.calls.argsFor(1);
......@@ -177,7 +176,7 @@ describe('Build', () => {
append: true,
});
expect(Turbolinks.visit).toHaveBeenCalledWith(BUILD_URL);
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
});
});
});
......
/* global Turbolinks */
//= require turbolinks
//= require lib/utils/url_utility
//= require lib/utils/common_utils
//= require filtered_search/filtered_search_token_keys
//= require filtered_search/filtered_search_tokenizer
......@@ -38,7 +36,7 @@
it('should search with a single word', () => {
getInput().value = 'searchTerm';
spyOn(Turbolinks, 'visit').and.callFake((url) => {
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
});
......@@ -48,7 +46,7 @@
it('should search with multiple words', () => {
getInput().value = 'awesome search terms';
spyOn(Turbolinks, 'visit').and.callFake((url) => {
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
});
......@@ -58,7 +56,7 @@
it('should search with special characters', () => {
getInput().value = '~!@#$%^&*()_+{}:<>,.?/';
spyOn(Turbolinks, 'visit').and.callFake((url) => {
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
});
......
/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
/* global Turbolinks */
/*= require jquery */
/*= require gl_dropdown */
/*= require turbolinks */
/*= require lib/utils/common_utils */
/*= require lib/utils/type_utility */
//= require lib/utils/url_utility
(() => {
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
......@@ -113,13 +112,13 @@
expect(this.dropdownContainerElement).toHaveClass('open');
const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => {
spyOn(Turbolinks, 'visit').and.stub();
spyOn(gl.utils, 'visitUrl').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
const linkedLocation = link.attr('href');
if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
});
......
/* global Issuable */
/* global Turbolinks */
//= require lib/utils/url_utility
//= require issuable
//= require turbolinks
(() => {
const BASE_URL = '/user/project/issues?scope=all&state=closed';
......@@ -42,39 +41,39 @@
});
it('should contain only the default parameters', () => {
spyOn(Turbolinks, 'visit');
spyOn(gl.utils, 'visitUrl');
Issuable.filterResults($filtersForm);
expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
});
it('should filter for the phrase "broken"', () => {
spyOn(Turbolinks, 'visit');
spyOn(gl.utils, 'visitUrl');
updateForm({ search: 'broken' }, $filtersForm);
Issuable.filterResults($filtersForm);
const params = `${DEFAULT_PARAMS}&search=broken`;
expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
});
it('should keep query parameters after modifying filter', () => {
spyOn(Turbolinks, 'visit');
spyOn(gl.utils, 'visitUrl');
// initial filter
updateForm({ milestone_title: 'v1.0' }, $filtersForm);
Issuable.filterResults($filtersForm);
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
// update filter
updateForm({ label_name: 'Frontend' }, $filtersForm);
Issuable.filterResults($filtersForm);
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
});
});
});
......
......@@ -21,5 +21,19 @@
expect(largeFont > regular).toBe(true);
});
});
describe('gl.text.pluralize', () => {
it('returns pluralized', () => {
expect(gl.text.pluralize('test', 2)).toBe('tests');
});
it('returns pluralized when count is 0', () => {
expect(gl.text.pluralize('test', 0)).toBe('tests');
});
it('does not return pluralized', () => {
expect(gl.text.pluralize('test', 1)).toBe('test');
});
});
});
})();
......@@ -99,7 +99,6 @@
});
newState = this.subject('commits');
expect(this.spies.history).toHaveBeenCalledWith({
turbolinks: true,
url: newState
}, document.title, newState);
});
......
......@@ -6,8 +6,6 @@
/*= require lib/utils/common_utils */
/*= require lib/utils/type_utility */
/*= require fuzzaldrin-plus */
/*= require turbolinks */
/*= require jquery.turbolinks */
(function() {
var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
......
......@@ -164,7 +164,7 @@
const interval = this.smartInterval;
setTimeout(() => {
$(document).trigger('page:before-unload');
$(document).trigger('beforeunload');
expect(interval.state.intervalId).toBeUndefined();
expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
done();
......
......@@ -8,7 +8,6 @@
// everything in application, however you may get better load performance if you
// require the specific files that are being used in the spec that tests them.
/*= require jquery */
/*= require jquery.turbolinks */
/*= require bootstrap */
/*= require underscore */
......
......@@ -5,15 +5,15 @@ describe EventFilter, lib: true do
let(:source_user) { create(:user) }
let!(:public_project) { create(:empty_project, :public) }
let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) }
let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) }
let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) }
let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) }
let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) }
let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) }
let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) }
let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) }
let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) }
let!(:push_event) { create(:event, :pushed, project: public_project, target: public_project, author: source_user) }
let!(:merged_event) { create(:event, :merged, project: public_project, target: public_project, author: source_user) }
let!(:created_event) { create(:event, :created, project: public_project, target: public_project, author: source_user) }
let!(:updated_event) { create(:event, :updated, project: public_project, target: public_project, author: source_user) }
let!(:closed_event) { create(:event, :closed, project: public_project, target: public_project, author: source_user) }
let!(:reopened_event) { create(:event, :reopened, project: public_project, target: public_project, author: source_user) }
let!(:comments_event) { create(:event, :commented, project: public_project, target: public_project, author: source_user) }
let!(:joined_event) { create(:event, :joined, project: public_project, target: public_project, author: source_user) }
let!(:left_event) { create(:event, :left, project: public_project, target: public_project, author: source_user) }
it 'applies push filter' do
events = EventFilter.new(EventFilter.push).apply_filter(Event.all)
......
......@@ -182,7 +182,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
project: project,
commit_id: ci_pipeline.sha)
create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
create(:event, :created, target: milestone, project: project, author: user)
create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
......
......@@ -19,7 +19,7 @@ describe Event, models: true do
let(:project) { create(:empty_project) }
it 'calls the reset_project_activity method' do
expect_any_instance_of(Event).to receive(:reset_project_activity)
expect_any_instance_of(described_class).to receive(:reset_project_activity)
create_event(project, project.owner)
end
......@@ -43,33 +43,33 @@ describe Event, models: true do
describe '#membership_changed?' do
context "created" do
subject { build(:event, action: Event::CREATED).membership_changed? }
subject { build(:event, :created).membership_changed? }
it { is_expected.to be_falsey }
end
context "updated" do
subject { build(:event, action: Event::UPDATED).membership_changed? }
subject { build(:event, :updated).membership_changed? }
it { is_expected.to be_falsey }
end
context "expired" do
subject { build(:event, action: Event::EXPIRED).membership_changed? }
subject { build(:event, :expired).membership_changed? }
it { is_expected.to be_truthy }
end
context "left" do
subject { build(:event, action: Event::LEFT).membership_changed? }
subject { build(:event, :left).membership_changed? }
it { is_expected.to be_truthy }
end
context "joined" do
subject { build(:event, action: Event::JOINED).membership_changed? }
subject { build(:event, :joined).membership_changed? }
it { is_expected.to be_truthy }
end
end
describe '#note?' do
subject { Event.new(project: target.project, target: target) }
subject { described_class.new(project: target.project, target: target) }
context 'issue note event' do
let(:target) { create(:note_on_issue) }
......@@ -97,7 +97,7 @@ describe Event, models: true do
let(:note_on_commit) { create(:note_on_commit, project: project) }
let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
let(:event) { Event.new(project: project, target: target, author_id: author.id) }
let(:event) { described_class.new(project: project, target: target, author_id: author.id) }
before do
project.team << [member, :developer]
......@@ -221,13 +221,13 @@ describe Event, models: true do
let!(:event2) { create(:closed_issue_event) }
describe 'without an explicit limit' do
subject { Event.limit_recent }
subject { described_class.limit_recent }
it { is_expected.to eq([event2, event1]) }
end
describe 'with an explicit limit' do
subject { Event.limit_recent(1) }
subject { described_class.limit_recent(1) }
it { is_expected.to eq([event2]) }
end
......@@ -294,9 +294,9 @@ describe Event, models: true do
}
}
Event.create({
described_class.create({
project: project,
action: Event::PUSHED,
action: described_class::PUSHED,
data: data,
author_id: user.id
}.merge!(attrs))
......
......@@ -19,13 +19,6 @@ describe List do
expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
end
context 'when list_type is set to backlog' do
subject { described_class.new(list_type: :backlog) }
it { is_expected.not_to validate_presence_of(:label) }
it { is_expected.not_to validate_presence_of(:position) }
end
context 'when list_type is set to done' do
subject { described_class.new(list_type: :done) }
......@@ -41,12 +34,6 @@ describe List do
expect(subject.destroy).to be_truthy
end
it 'can not be destroyed when list_type is set to backlog' do
subject = create(:backlog_list)
expect(subject.destroy).to be_falsey
end
it 'can not be destroyed when when list_type is set to done' do
subject = create(:done_list)
......@@ -55,19 +42,13 @@ describe List do
end
describe '#destroyable?' do
it 'retruns true when list_type is set to label' do
it 'returns true when list_type is set to label' do
subject.list_type = :label
expect(subject).to be_destroyable
end
it 'retruns false when list_type is set to backlog' do
subject.list_type = :backlog
expect(subject).not_to be_destroyable
end
it 'retruns false when list_type is set to done' do
it 'returns false when list_type is set to done' do
subject.list_type = :done
expect(subject).not_to be_destroyable
......@@ -75,19 +56,13 @@ describe List do
end
describe '#movable?' do
it 'retruns true when list_type is set to label' do
it 'returns true when list_type is set to label' do
subject.list_type = :label
expect(subject).to be_movable
end
it 'retruns false when list_type is set to backlog' do
subject.list_type = :backlog
expect(subject).not_to be_movable
end
it 'retruns false when list_type is set to done' do
it 'returns false when list_type is set to done' do
subject.list_type = :done
expect(subject).not_to be_movable
......@@ -102,12 +77,6 @@ describe List do
expect(subject.title).to eq 'Development'
end
it 'returns Backlog when list_type is set to backlog' do
subject.list_type = :backlog
expect(subject.title).to eq 'Backlog'
end
it 'returns Done when list_type is set to done' do
subject.list_type = :done
......
......@@ -1013,8 +1013,8 @@ describe User, models: true do
let!(:project2) { create(:empty_project, forked_from_project: project3) }
let!(:project3) { create(:empty_project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
let!(:push_event) { create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject) }
let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) }
let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) }
let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
before do
project1.team << [subject, :master]
......@@ -1058,7 +1058,7 @@ describe User, models: true do
let!(:push_data) do
Gitlab::DataBuilder::Push.build_sample(project2, subject)
end
let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) }
let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) }
before do
project1.team << [subject, :master]
......@@ -1086,7 +1086,7 @@ describe User, models: true do
expect(subject.recent_push(project2)).to eq(push_event)
push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1)
push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1)
expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
end
......
......@@ -11,12 +11,11 @@ describe Boards::CreateService, services: true do
expect { service.execute }.to change(Board, :count).by(1)
end
it 'creates default lists' do
it 'creates the default lists' do
board = service.execute
expect(board.lists.size).to eq 2
expect(board.lists.first).to be_backlog
expect(board.lists.last).to be_done
expect(board.lists.size).to eq 1
expect(board.lists.first).to be_done
end
end
......
......@@ -13,7 +13,6 @@ describe Boards::Issues::ListService, services: true do
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
let!(:backlog) { create(:backlog_list, board: board) }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board) }
......@@ -45,8 +44,8 @@ describe Boards::Issues::ListService, services: true do
end
context 'sets default order to priority' do
it 'returns opened issues when listing issues from Backlog' do
params = { board_id: board.id, id: backlog.id }
it 'returns opened issues when list id is missing' do
params = { board_id: board.id }
issues = described_class.new(project, user, params).execute
......
......@@ -10,7 +10,6 @@ describe Boards::Issues::MoveService, services: true do
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
let!(:backlog) { create(:backlog_list, board: board1) }
let!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board1) }
......@@ -19,41 +18,6 @@ describe Boards::Issues::MoveService, services: true do
project.team << [user, :developer]
end
context 'when moving from backlog' do
it 'adds the label of the list it goes to' do
issue = create(:labeled_issue, project: project, labels: [bug])
params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: list1.id }
described_class.new(project, user, params).execute(issue)
expect(issue.reload.labels).to contain_exactly(bug, development)
end
end
context 'when moving to backlog' do
it 'removes all list-labels' do
issue = create(:labeled_issue, project: project, labels: [bug, development, testing])
params = { board_id: board1.id, from_list_id: list1.id, to_list_id: backlog.id }
described_class.new(project, user, params).execute(issue)
expect(issue.reload.labels).to contain_exactly(bug)
end
end
context 'when moving from backlog to done' do
it 'closes the issue' do
issue = create(:labeled_issue, project: project, labels: [bug])
params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: done.id }
described_class.new(project, user, params).execute(issue)
issue.reload
expect(issue.labels).to contain_exactly(bug)
expect(issue).to be_closed
end
end
context 'when moving an issue between lists' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
......@@ -113,19 +77,6 @@ describe Boards::Issues::MoveService, services: true do
end
end
context 'when moving from done to backlog' do
it 'reopens the issue' do
issue = create(:labeled_issue, :closed, project: project, labels: [bug])
params = { board_id: board1.id, from_list_id: done.id, to_list_id: backlog.id }
described_class.new(project, user, params).execute(issue)
issue.reload
expect(issue.labels).to contain_exactly(bug)
expect(issue).to be_reopened
end
end
context 'when moving to same list' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
......
......@@ -21,7 +21,7 @@ describe Boards::Lists::CreateService, services: true do
end
end
context 'when board lists has backlog, and done lists' do
context 'when board lists has the done list' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
......@@ -40,7 +40,7 @@ describe Boards::Lists::CreateService, services: true do
end
end
context 'when board lists has backlog, label and done lists' do
context 'when board lists has label and done lists' do
it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0)
......
......@@ -15,7 +15,6 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'decrements position of higher lists' do
backlog = board.backlog_list
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
......@@ -23,20 +22,12 @@ describe Boards::Lists::DestroyService, services: true do
described_class.new(project, user).execute(development)
expect(backlog.reload.position).to be_nil
expect(review.reload.position).to eq 0
expect(staging.reload.position).to eq 1
expect(done.reload.position).to be_nil
end
end
it 'does not remove list from board when list type is backlog' do
list = board.backlog_list
service = described_class.new(project, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
end
it 'does not remove list from board when list type is done' do
list = board.done_list
service = described_class.new(project, user)
......
......@@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do
service = described_class.new(project, double)
expect(service.execute(board)).to eq [board.backlog_list, list, board.done_list]
expect(service.execute(board)).to eq [list, board.done_list]
end
end
end
......@@ -6,7 +6,6 @@ describe Boards::Lists::MoveService, services: true do
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let!(:backlog) { create(:backlog_list, board: board) }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
......@@ -87,14 +86,6 @@ describe Boards::Lists::MoveService, services: true do
end
end
it 'keeps position of lists when list type is backlog' do
service = described_class.new(project, user, position: 2)
service.execute(backlog)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when list type is done' do
service = described_class.new(project, user, position: 2)
......
......@@ -653,5 +653,37 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue }
end
end
context '/target_branch command' do
let(:non_empty_project) { create(:project) }
let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
let(:service) { described_class.new(non_empty_project, developer)}
it 'updates target_branch if /target_branch command is executed' do
_, updates = service.execute('/target_branch merge-test', merge_request)
expect(updates).to eq(target_branch: 'merge-test')
end
it 'handles blanks around param' do
_, updates = service.execute('/target_branch merge-test ', merge_request)
expect(updates).to eq(target_branch: 'merge-test')
end
context 'ignores command with no argument' do
it_behaves_like 'empty command' do
let(:content) { '/target_branch' }
let(:issuable) { another_merge_request }
end
end
context 'ignores non-existing target branch' do
it_behaves_like 'empty command' do
let(:content) { '/target_branch totally_non_existing_branch' }
let(:issuable) { another_merge_request }
end
end
end
end
end
......@@ -35,7 +35,7 @@ module ExportFileHelper
project: project,
commit_id: ci_pipeline.sha)
create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
create(:event, :created, target: milestone, project: project, author: user)
create(:project_member, :master, user: user, project: project)
create(:ci_variable, project: project)
create(:ci_trigger, project: project)
......
// Generated by CoffeeScript 1.7.1
/*
jQuery.Turbolinks ~ https://github.com/kossnocorp/jquery.turbolinks
jQuery plugin for drop-in fix binded events problem caused by Turbolinks
The MIT License
Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz
*/
(function() {
var $, $document;
$ = window.jQuery || (typeof require === "function" ? require('jquery') : void 0);
$document = $(document);
$.turbo = {
version: '2.1.0',
isReady: false,
use: function(load, fetch) {
return $document.off('.turbo').on("" + load + ".turbo", this.onLoad).on("" + fetch + ".turbo", this.onFetch);
},
addCallback: function(callback) {
if ($.turbo.isReady) {
callback($);
}
return $document.on('turbo:ready', function() {
return callback($);
});
},
onLoad: function() {
$.turbo.isReady = true;
return $document.trigger('turbo:ready');
},
onFetch: function() {
return $.turbo.isReady = false;
},
register: function() {
$(this.onLoad);
return $.fn.ready = this.addCallback;
}
};
$.turbo.register();
$.turbo.use('page:load', 'page:fetch');
}).call(this);
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