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' ...@@ -222,7 +222,6 @@ gem 'chronic_duration', '~> 0.10.6'
gem 'sass-rails', '~> 5.0.6' gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0' gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2' gem 'uglifier', '~> 2.7.2'
gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0' gem 'bootstrap-sass', '~> 3.3.0'
......
...@@ -266,8 +266,6 @@ GEM ...@@ -266,8 +266,6 @@ GEM
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-markup (1.5.1) gitlab-markup (1.5.1)
gitlab-turbolinks-classic (2.5.6)
coffee-rails
gitlab_omniauth-ldap (1.2.1) gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9) net-ldap (~> 0.9)
omniauth (~> 1.0) omniauth (~> 1.0)
...@@ -891,7 +889,6 @@ DEPENDENCIES ...@@ -891,7 +889,6 @@ DEPENDENCIES
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
gitlab-turbolinks-classic (~> 2.5, >= 2.5.6)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.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 */ /* 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() { (function() {
this.Admin = (function() { this.Admin = (function() {
...@@ -42,10 +41,10 @@ ...@@ -42,10 +41,10 @@
return $('.change-owner-link').show(); return $('.change-owner-link').show();
}); });
$('li.project_member').bind('ajax:success', function() { $('li.project_member').bind('ajax:success', function() {
return Turbolinks.visit(location.href); return gl.utils.refreshCurrentPage();
}); });
$('li.group_member').bind('ajax:success', function() { $('li.group_member').bind('ajax:success', function() {
return Turbolinks.visit(location.href); return gl.utils.refreshCurrentPage();
}); });
showBlacklistType = function() { showBlacklistType = function() {
if ($("input[name='blacklist_type']:checked").val() === 'file') { if ($("input[name='blacklist_type']:checked").val() === 'file') {
......
...@@ -24,9 +24,7 @@ ...@@ -24,9 +24,7 @@
/*= require jquery.waitforimages */ /*= require jquery.waitforimages */
/*= require jquery.atwho */ /*= require jquery.atwho */
/*= require jquery.scrollTo */ /*= require jquery.scrollTo */
/*= require jquery.turbolinks */
/*= require js.cookie */ /*= require js.cookie */
/*= require turbolinks */
/*= require autosave */ /*= require autosave */
/*= require bootstrap/affix */ /*= require bootstrap/affix */
/*= require bootstrap/alert */ /*= require bootstrap/alert */
...@@ -64,7 +62,7 @@ ...@@ -64,7 +62,7 @@
/*= require es6-promise.auto */ /*= require es6-promise.auto */
(function () { (function () {
document.addEventListener('page:fetch', function () { document.addEventListener('beforeunload', function () {
// Unbind scroll events // Unbind scroll events
$(document).off('scroll'); $(document).off('scroll');
// Close any open tooltips // Close any open tooltips
......
...@@ -14,10 +14,12 @@ ...@@ -14,10 +14,12 @@
//= require ./components/board_sidebar //= require ./components/board_sidebar
//= require ./components/new_list_dropdown //= require ./components/new_list_dropdown
//= require vue_shared/vue_resource_interceptor //= require vue_shared/vue_resource_interceptor
//= require ./components/modal/index
$(() => { $(() => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
...@@ -31,7 +33,8 @@ $(() => { ...@@ -31,7 +33,8 @@ $(() => {
el: $boardApp, el: $boardApp,
components: { components: {
'board': gl.issueBoards.Board, 'board': gl.issueBoards.Board,
'board-sidebar': gl.issueBoards.BoardSidebar 'board-sidebar': gl.issueBoards.BoardSidebar,
'board-add-issues-modal': gl.issueBoards.IssuesModal,
}, },
data: { data: {
state: Store.state, state: Store.state,
...@@ -40,6 +43,8 @@ $(() => { ...@@ -40,6 +43,8 @@ $(() => {
boardId: $boardApp.dataset.boardId, boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true', disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase, issueLinkBase: $boardApp.dataset.issueLinkBase,
rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: Store.detail detailIssue: Store.detail
}, },
computed: { computed: {
...@@ -48,7 +53,7 @@ $(() => { ...@@ -48,7 +53,7 @@ $(() => {
}, },
}, },
created () { created () {
gl.boardService = new BoardService(this.endpoint, this.boardId); gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
}, },
mounted () { mounted () {
Store.disabled = this.disabled; Store.disabled = this.disabled;
...@@ -59,8 +64,6 @@ $(() => { ...@@ -59,8 +64,6 @@ $(() => {
if (list.type === 'done') { if (list.type === 'done') {
list.position = Infinity; list.position = Infinity;
} else if (list.type === 'backlog') {
list.position = -1;
} }
}); });
...@@ -81,4 +84,27 @@ $(() => { ...@@ -81,4 +84,27 @@ $(() => {
gl.issueBoards.newListDropdownInit(); 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 @@ ...@@ -22,7 +22,8 @@
props: { props: {
list: Object, list: Object,
disabled: Boolean, disabled: Boolean,
issueLinkBase: String issueLinkBase: String,
rootPath: String,
}, },
data () { data () {
return { return {
......
/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ /* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
//= require ./issue_card_inner
/* global Vue */ /* global Vue */
(() => { (() => {
...@@ -9,12 +10,16 @@ ...@@ -9,12 +10,16 @@
gl.issueBoards.BoardCard = Vue.extend({ gl.issueBoards.BoardCard = Vue.extend({
template: '#js-board-list-card', template: '#js-board-list-card',
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
props: { props: {
list: Object, list: Object,
issue: Object, issue: Object,
issueLinkBase: String, issueLinkBase: String,
disabled: Boolean, disabled: Boolean,
index: Number index: Number,
rootPath: String,
}, },
data () { data () {
return { return {
...@@ -28,31 +33,6 @@ ...@@ -28,31 +33,6 @@
} }
}, },
methods: { 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 () { mouseDown () {
this.showDetail = true; this.showDetail = true;
}, },
...@@ -71,6 +51,7 @@ ...@@ -71,6 +51,7 @@
Store.detail.issue = {}; Store.detail.issue = {};
} else { } else {
Store.detail.issue = this.issue; Store.detail.issue = this.issue;
Store.detail.list = this.list;
} }
} }
} }
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
issues: Array, issues: Array,
loading: Boolean, loading: Boolean,
issueLinkBase: String, issueLinkBase: String,
rootPath: String,
}, },
data () { data () {
return { return {
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
Store.detail.issue = issue; Store.detail.issue = issue;
Store.detail.list = this.list;
}) })
.catch(() => { .catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global LabelsSelect */ /* global LabelsSelect */
/* global Sidebar */ /* global Sidebar */
//= require ./sidebar/remove_issue
(() => { (() => {
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -18,7 +19,8 @@ ...@@ -18,7 +19,8 @@
data() { data() {
return { return {
detail: Store.detail, detail: Store.detail,
issue: {} issue: {},
list: {},
}; };
}, },
computed: { computed: {
...@@ -36,6 +38,7 @@ ...@@ -36,6 +38,7 @@
} }
this.issue = this.detail.issue; this.issue = this.detail.issue;
this.list = this.detail.list;
}, },
deep: true deep: true
}, },
...@@ -60,6 +63,9 @@ ...@@ -60,6 +63,9 @@
new LabelsSelect(); new LabelsSelect();
new Sidebar(); new Sidebar();
gl.Subscription.bindAll('.subscription'); 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 @@ ...@@ -6,12 +6,15 @@
class ListIssue { class ListIssue {
constructor (obj) { constructor (obj) {
this.globalId = obj.id;
this.id = obj.iid; this.id = obj.iid;
this.title = obj.title; this.title = obj.title;
this.confidential = obj.confidential; this.confidential = obj.confidential;
this.dueDate = obj.due_date; this.dueDate = obj.due_date;
this.subscribed = obj.subscribed; this.subscribed = obj.subscribed;
this.labels = []; this.labels = [];
this.selected = false;
this.assignee = false;
if (obj.assignee) { if (obj.assignee) {
this.assignee = new ListUser(obj.assignee); this.assignee = new ListUser(obj.assignee);
......
...@@ -9,7 +9,7 @@ class List { ...@@ -9,7 +9,7 @@ class List {
this.position = obj.position; this.position = obj.position;
this.title = obj.title; this.title = obj.title;
this.type = obj.list_type; 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.filters = gl.issueBoards.BoardsStore.state.filters;
this.page = 1; this.page = 1;
this.loading = true; this.loading = true;
......
...@@ -2,7 +2,13 @@ ...@@ -2,7 +2,13 @@
/* global Vue */ /* global Vue */
class BoardService { 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}`, {}, { this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: { generate: {
method: 'POST', method: 'POST',
...@@ -10,7 +16,12 @@ class BoardService { ...@@ -10,7 +16,12 @@ class BoardService {
} }
}); });
this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); 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) => { Vue.http.interceptors.push((request, next) => {
request.headers['X-CSRF-Token'] = $.rails.csrfToken(); request.headers['X-CSRF-Token'] = $.rails.csrfToken();
...@@ -65,6 +76,20 @@ class BoardService { ...@@ -65,6 +76,20 @@ class BoardService {
issue 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; window.BoardService = BoardService;
...@@ -34,15 +34,10 @@ ...@@ -34,15 +34,10 @@
}, },
new (listObj) { new (listObj) {
const list = this.addList(listObj); const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
list list
.save() .save()
.then(() => { .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.state.lists = _.sortBy(this.state.lists, 'position');
}); });
this.removeBlankState(); this.removeBlankState();
...@@ -52,7 +47,7 @@ ...@@ -52,7 +47,7 @@
}, },
shouldAddBlankState () { shouldAddBlankState () {
// Decide whether to add the blank state // Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]); return !(this.state.lists.filter(list => list.type !== 'done')[0]);
}, },
addBlankState () { addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
...@@ -102,7 +97,7 @@ ...@@ -102,7 +97,7 @@
listTo.addIssue(issue, listFrom, newIndex); listTo.addIssue(issue, listFrom, newIndex);
} }
if (listTo.type === 'done' && listFrom.type !== 'backlog') { if (listTo.type === 'done') {
issueLists.forEach((list) => { issueLists.forEach((list) => {
list.removeIssue(issue); 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 @@ ...@@ -43,6 +43,7 @@
BreakpointInstance.prototype.getBreakpointSize = function() { BreakpointInstance.prototype.getBreakpointSize = function() {
var $visibleDevice; var $visibleDevice;
$visibleDevice = this.visibleDevice; $visibleDevice = this.visibleDevice;
// TODO: Consider refactoring in light of turbolinks removal.
// the page refreshed via turbolinks // the page refreshed via turbolinks
if (!$visibleDevice().length) { if (!$visibleDevice().length) {
this.setup(); 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 */ /* 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 Breakpoints */
/* global Turbolinks */
(function() { (function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
...@@ -127,7 +126,7 @@ ...@@ -127,7 +126,7 @@
pageUrl += DOWN_BUILD_TRACE; pageUrl += DOWN_BUILD_TRACE;
} }
return Turbolinks.visit(pageUrl); return gl.utils.visitUrl(pageUrl);
} }
}; };
})(this) })(this)
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
(() => { (() => {
const UNFOLD_COUNT = 20; const UNFOLD_COUNT = 20;
let isBound = false;
class Diff { class Diff {
constructor() { constructor() {
...@@ -17,10 +18,12 @@ ...@@ -17,10 +18,12 @@
$('.content-wrapper .container-fluid').removeClass('container-limited'); $('.content-wrapper .container-fluid').removeClass('container-limited');
} }
if (!isBound) {
$(document) $(document)
.off('click', '.js-unfold, .diff-line-num a')
.on('click', '.js-unfold', this.handleClickUnfold.bind(this)) .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
.on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
isBound = true;
}
this.openAnchoredDiff(); this.openAnchoredDiff();
} }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
this.setupMapping(); this.setupMapping();
this.cleanupWrapper = this.cleanup.bind(this); this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('page:fetch', this.cleanupWrapper); document.addEventListener('beforeunload', this.cleanupWrapper);
} }
cleanup() { cleanup() {
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
this.setupMapping(); this.setupMapping();
document.removeEventListener('page:fetch', this.cleanupWrapper); document.removeEventListener('beforeunload', this.cleanupWrapper);
} }
setupMapping() { setupMapping() {
......
/* global Turbolinks */
(() => { (() => {
class FilteredSearchManager { class FilteredSearchManager {
constructor() { constructor() {
...@@ -15,13 +13,13 @@ ...@@ -15,13 +13,13 @@
this.dropdownManager.setDropdown(); this.dropdownManager.setDropdown();
this.cleanupWrapper = this.cleanup.bind(this); this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('page:fetch', this.cleanupWrapper); document.addEventListener('beforeunload', this.cleanupWrapper);
} }
} }
cleanup() { cleanup() {
this.unbindEvents(); this.unbindEvents();
document.removeEventListener('page:fetch', this.cleanupWrapper); document.removeEventListener('beforeunload', this.cleanupWrapper);
} }
bindEvents() { bindEvents() {
...@@ -200,7 +198,9 @@ ...@@ -200,7 +198,9 @@
paths.push(`search=${sanitized}`); paths.push(`search=${sanitized}`);
} }
Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`;
gl.utils.visitUrl(parameterizedUrl);
} }
getUsernameParams() { 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 */ /* 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 fuzzaldrinPlus */
/* global Turbolinks */
(function() { (function() {
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
...@@ -723,7 +722,7 @@ ...@@ -723,7 +722,7 @@
if ($el.length) { if ($el.length) {
var href = $el.attr('href'); var href = $el.attr('href');
if (href && href !== '#') { if (href && href !== '#') {
Turbolinks.visit(href); gl.utils.visitUrl(href);
} else { } else {
$el.first().trigger('click'); $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 */ /* 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 Issuable */
/* global Turbolinks */
((global) => { ((global) => {
var issuable_created; var issuable_created;
...@@ -119,7 +118,7 @@ ...@@ -119,7 +118,7 @@
issuesUrl = formAction; issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
issuesUrl += formData; issuesUrl += formData;
return Turbolinks.visit(issuesUrl); return gl.utils.visitUrl(issuesUrl);
}; };
})(this), })(this),
initResetFilters: function() { initResetFilters: function() {
...@@ -130,7 +129,7 @@ ...@@ -130,7 +129,7 @@
const baseIssuesUrl = target.href; const baseIssuesUrl = target.href;
$form.attr('action', baseIssuesUrl); $form.attr('action', baseIssuesUrl);
Turbolinks.visit(baseIssuesUrl); gl.utils.visitUrl(baseIssuesUrl);
}); });
}, },
initChecks: function() { initChecks: function() {
......
...@@ -95,7 +95,6 @@ ...@@ -95,7 +95,6 @@
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
history.replaceState({ history.replaceState({
turbolinks: true,
url: newState, url: newState,
}, document.title, newState); }, document.title, newState);
return newState; return newState;
......
...@@ -161,6 +161,9 @@ ...@@ -161,6 +161,9 @@
gl.text.humanize = function(string) { gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); 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 gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...'; return string.substr(0, (maxLength - 3)) + '...';
}; };
......
...@@ -76,5 +76,11 @@ ...@@ -76,5 +76,11 @@
hashIndex = url.indexOf('#'); hashIndex = url.indexOf('#');
return hashIndex === -1 ? null : url.substring(hashIndex + 1); 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); })(window);
}).call(this); }).call(this);
...@@ -171,7 +171,6 @@ ...@@ -171,7 +171,6 @@
// This method is stubbed in tests. // This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) { LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({ return history.pushState({
turbolinks: false,
url: value url: value
// We're using pushState instead of assigning location.hash directly to // We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event // prevent the page from scrolling on the hashchange event
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
/* global Turbolinks */
(function() { (function() {
Turbolinks.enableProgressBar(); window.addEventListener('beforeunload', function() {
$(document).on('page:fetch', function() {
$('.tanuki-logo').addClass('animate'); $('.tanuki-logo').addClass('animate');
}); });
$(document).on('page:change', function() {
$('.tanuki-logo').removeClass('animate');
});
}).call(this); }).call(this);
...@@ -179,12 +179,13 @@ ...@@ -179,12 +179,13 @@
// Ensure parameters and hash come along for the ride // Ensure parameters and hash come along for the ride
newState += location.search + location.hash; newState += location.search + location.hash;
// TODO: Consider refactoring in light of turbolinks removal.
// Replace the current history state with the new one without breaking // Replace the current history state with the new one without breaking
// Turbolinks' history. // Turbolinks' history.
// //
// See https://github.com/rails/turbolinks/issues/363 // See https://github.com/rails/turbolinks/issues/363
window.history.replaceState({ window.history.replaceState({
turbolinks: true,
url: newState, url: newState,
}, document.title, newState); }, document.title, newState);
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
/* global notify */ /* global notify */
/* global notifyPermissions */ /* global notifyPermissions */
/* global merge_request_widget */ /* global merge_request_widget */
/* global Turbolinks */
((global) => { ((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; }; 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 @@ ...@@ -69,13 +68,13 @@
} }
MergeRequestWidget.prototype.clearEventListeners = function() { MergeRequestWidget.prototype.clearEventListeners = function() {
return $(document).off('page:change.merge_request'); return $(document).off('DOMContentLoaded');
}; };
MergeRequestWidget.prototype.addEventListeners = function() { MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages; var allowedPages;
allowedPages = ['show', 'commits', 'pipelines', 'changes']; allowedPages = ['show', 'commits', 'pipelines', 'changes'];
$(document).on('page:change.merge_request', (function(_this) { $(document).on('DOMContentLoaded', (function(_this) {
return function() { return function() {
var page; var page;
page = $('body').data('page').split(':').last(); 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 */ /* 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 Cookies */
/* global Turbolinks */
/* global ProjectSelect */ /* global ProjectSelect */
(function() { (function() {
...@@ -58,8 +57,8 @@ ...@@ -58,8 +57,8 @@
}; };
Project.prototype.initRefSwitcher = function() { Project.prototype.initRefSwitcher = function() {
var refListItem = document.createElement('li'), var refListItem = document.createElement('li');
refLink = document.createElement('a'); var refLink = document.createElement('a');
refLink.href = '#'; refLink.href = '#';
...@@ -118,7 +117,7 @@ ...@@ -118,7 +117,7 @@
var $form = $dropdown.closest('form'); var $form = $dropdown.closest('form');
var action = $form.attr('action'); var action = $form.attr('action');
var divider = action.indexOf('?') < 0 ? '?' : '&'; 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 */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
/* global Turbolinks */
(function() { (function() {
this.ProjectImport = (function() { this.ProjectImport = (function() {
function ProjectImport() { function ProjectImport() {
setTimeout(function() { setTimeout(function() {
return Turbolinks.visit(location.href); return gl.utils.visitUrl(location.href);
}, 5000); }, 5000);
} }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
this.find('.js-render-math').renderMath(); this.find('.js-render-math').renderMath();
}; };
$(document).on('ready page:load', function() { $(document).on('ready load', function() {
return $('body').renderGFM(); return $('body').renderGFM();
}); });
}).call(this); }).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 */ /* 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 Mousetrap */
/* global Turbolinks */
/* global findFileURL */ /* global findFileURL */
(function() { (function() {
...@@ -23,7 +22,7 @@ ...@@ -23,7 +22,7 @@
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
if (typeof findFileURL !== "undefined" && findFileURL !== null) { if (typeof findFileURL !== "undefined" && findFileURL !== null) {
Mousetrap.bind('t', function() { 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 */ /* 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 Mousetrap */
/* global Turbolinks */
/* global ShortcutsNavigation */ /* global ShortcutsNavigation */
/* global sidebar */ /* global sidebar */
...@@ -80,7 +79,7 @@ ...@@ -80,7 +79,7 @@
ShortcutsIssuable.prototype.editIssue = function() { ShortcutsIssuable.prototype.editIssue = function() {
var $editBtn; var $editBtn;
$editBtn = $('.issuable-edit'); $editBtn = $('.issuable-edit');
return Turbolinks.visit($editBtn.attr('href')); return gl.utils.visitUrl($editBtn.attr('href'));
}; };
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
.on('click', sidebarToggleSelector, () => this.toggleSidebar()) .on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState()) .on('click', pinnedToggleSelector, () => this.togglePinnedState())
.on('click', 'html, body', (e) => this.handleClickEvent(e)) .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)); .on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState(); this.renderState();
} }
......
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
destroy() { destroy() {
this.cancel(); this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange); document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('page:before-unload'); $(document).off('visibilitychange').off('beforeunload');
} }
/* private */ /* private */
...@@ -111,8 +111,9 @@ ...@@ -111,8 +111,9 @@
} }
initPageUnloadHandling() { initPageUnloadHandling() {
// TODO: Consider refactoring in light of turbolinks removal.
// prevent interval continuing after page change, when kept in cache by Turbolinks // 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) { 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 */ /* 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 UsersSelect */
/* global Turbolinks */
((global) => { ((global) => {
class Todos { class Todos {
...@@ -34,7 +33,7 @@ ...@@ -34,7 +33,7 @@
$('form.filter-form').on('submit', function (event) { $('form.filter-form').on('submit', function (event) {
event.preventDefault(); event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize()); gl.utils.visitUrl(this.action + '&' + $(this).serialize());
}); });
} }
...@@ -142,7 +141,7 @@ ...@@ -142,7 +141,7 @@
}; };
url = gl.utils.mergeUrlParams(pageParams, url); url = gl.utils.mergeUrlParams(pageParams, url);
} }
return Turbolinks.visit(url); return gl.utils.visitUrl(url);
} }
} }
...@@ -156,7 +155,7 @@ ...@@ -156,7 +155,7 @@
e.preventDefault(); e.preventDefault();
return window.open(todoLink, '_blank'); return window.open(todoLink, '_blank');
} else { } 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 */ /* 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() { (function() {
this.TreeView = (function() { this.TreeView = (function() {
function TreeView() { function TreeView() {
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
e.preventDefault(); e.preventDefault();
return window.open(path, '_blank'); return window.open(path, '_blank');
} else { } else {
return Turbolinks.visit(path); return gl.utils.visitUrl(path);
} }
} }
}); });
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
} else if (e.which === 13) { } else if (e.which === 13) {
path = $('.tree-item.selected .tree-item-file-name a').attr('href'); path = $('.tree-item.selected .tree-item-file-name a').attr('href');
if (path) { if (path) {
return Turbolinks.visit(path); return gl.utils.visitUrl(path);
} }
} }
}); });
......
...@@ -149,7 +149,6 @@ content on the Users#show page. ...@@ -149,7 +149,6 @@ content on the Users#show page.
new_state = new_state.replace(/\/+$/, ''); new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash; new_state += this._location.search + this._location.hash;
history.replaceState({ history.replaceState({
turbolinks: true,
url: new_state url: new_state
}, document.title, new_state); }, document.title, new_state);
return new_state; return new_state;
......
/* global Vue, Turbolinks, gl */ /* global Vue, gl */
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
//= require vue_shared/components/table_pagination //= require vue_shared/components/table_pagination
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
}, },
methods: { methods: {
change(pagenum, apiScope) { change(pagenum, apiScope) {
Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
}, },
}, },
template: ` template: `
......
...@@ -7,12 +7,12 @@ ...@@ -7,12 +7,12 @@
window.removeEventListener('beforeunload', removeIntervals); window.removeEventListener('beforeunload', removeIntervals);
window.removeEventListener('focus', startIntervals); window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals); window.removeEventListener('blur', removeIntervals);
document.removeEventListener('page:fetch', removeAll); document.removeEventListener('beforeunload', removeAll);
}; };
window.addEventListener('beforeunload', removeIntervals); window.addEventListener('beforeunload', removeIntervals);
window.addEventListener('focus', startIntervals); window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals); window.addEventListener('blur', removeIntervals);
document.addEventListener('page:fetch', removeAll); document.addEventListener('beforeunload', removeAll);
}; };
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
gl.VueGlPagination = Vue.extend({ gl.VueGlPagination = Vue.extend({
props: { props: {
// TODO: Consider refactoring in light of turbolinks removal.
/** /**
This function will take the information given by the pagination component This function will take the information given by the pagination component
And make a new Turbolinks call And make a new Turbolinks call
...@@ -20,7 +22,7 @@ ...@@ -20,7 +22,7 @@
Here is an example `change` method: Here is an example `change` method:
change(pagenum, apiScope) { change(pagenum, apiScope) {
Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
}, },
*/ */
......
...@@ -31,7 +31,6 @@ ...@@ -31,7 +31,6 @@
@import "framework/modal.scss"; @import "framework/modal.scss";
@import "framework/nav.scss"; @import "framework/nav.scss";
@import "framework/pagination.scss"; @import "framework/pagination.scss";
@import "framework/progress.scss";
@import "framework/panels.scss"; @import "framework/panels.scss";
@import "framework/selects.scss"; @import "framework/selects.scss";
@import "framework/sidebar.scss"; @import "framework/sidebar.scss";
......
...@@ -227,6 +227,11 @@ ...@@ -227,6 +227,11 @@
} }
} }
.dropdown-menu-drop-up {
top: auto;
bottom: 100%;
}
.dropdown-menu-large { .dropdown-menu-large {
width: 340px; 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 @@ ...@@ -250,7 +250,7 @@
} }
.issue-boards-search { .issue-boards-search {
width: 290px; width: 395px;
.form-control { .form-control {
display: inline-block; display: inline-block;
...@@ -354,3 +354,135 @@ ...@@ -354,3 +354,135 @@
padding-right: 0; 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 ...@@ -10,10 +10,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) @projects = @projects.page(params[:page])
@last_push = current_user.recent_push
respond_to do |format| respond_to do |format|
format.html format.html { @last_push = current_user.recent_push }
format.atom do format.atom do
event_filter event_filter
load_events load_events
......
...@@ -7,7 +7,7 @@ module Projects ...@@ -7,7 +7,7 @@ module Projects
def index def index
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute 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: { render json: {
issues: serialize_as_json(issues), issues: serialize_as_json(issues),
...@@ -59,7 +59,7 @@ module Projects ...@@ -59,7 +59,7 @@ module Projects
end end
def filter_params 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 end
def move_params def move_params
...@@ -73,7 +73,7 @@ module Projects ...@@ -73,7 +73,7 @@ module Projects
def serialize_as_json(resource) def serialize_as_json(resource)
resource.as_json( resource.as_json(
labels: true, labels: true,
only: [:iid, :title, :confidential, :due_date], only: [:id, :iid, :title, :confidential, :due_date],
include: { include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] } milestone: { only: [:id, :title] }
......
...@@ -6,7 +6,9 @@ module BoardsHelper ...@@ -6,7 +6,9 @@ module BoardsHelper
endpoint: namespace_project_boards_path(@project.namespace, @project), endpoint: namespace_project_boards_path(@project.namespace, @project),
board_id: board.id, board_id: board.id,
disabled: "#{!can?(current_user, :admin_list, @project)}", 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
end end
module JavascriptHelper module JavascriptHelper
def page_specific_javascript_tag(js) def page_specific_javascript_tag(js)
javascript_include_tag asset_path(js), { "data-turbolinks-track" => true } javascript_include_tag asset_path(js)
end end
end end
...@@ -5,10 +5,6 @@ class Board < ActiveRecord::Base ...@@ -5,10 +5,6 @@ class Board < ActiveRecord::Base
validates :project, presence: true validates :project, presence: true
def backlog_list
lists.merge(List.backlog).take
end
def done_list def done_list
lists.merge(List.done).take lists.merge(List.done).take
end end
......
...@@ -2,7 +2,7 @@ class List < ActiveRecord::Base ...@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board belongs_to :board
belongs_to :label 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 :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label? validates :label, :position, presence: true, if: :label?
......
...@@ -12,7 +12,6 @@ module Boards ...@@ -12,7 +12,6 @@ module Boards
def create_board! def create_board!
board = project.boards.create board = project.boards.create
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done) board.lists.create(list_type: :done)
board board
......
...@@ -3,8 +3,8 @@ module Boards ...@@ -3,8 +3,8 @@ module Boards
class ListService < BaseService class ListService < BaseService
def execute def execute
issues = IssuesFinder.new(current_user, filter_params).execute issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless list.movable? issues = without_board_labels(issues) unless movable_list?
issues = with_list_label(issues) if list.movable? issues = with_list_label(issues) if movable_list?
issues issues
end end
...@@ -15,7 +15,13 @@ module Boards ...@@ -15,7 +15,13 @@ module Boards
end end
def list 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 end
def filter_params def filter_params
...@@ -40,7 +46,7 @@ module Boards ...@@ -40,7 +46,7 @@ module Boards
end end
def set_state def set_state
params[:state] = list.done? ? 'closed' : 'opened' params[:state] = list && list.done? ? 'closed' : 'opened'
end end
def board_label_ids def board_label_ids
......
...@@ -304,6 +304,18 @@ module SlashCommands ...@@ -304,6 +304,18 @@ module SlashCommands
params '@user' params '@user'
command :cc 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) def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id) 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) labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
- providers.each do |provider| - providers.each do |provider|
%span.light %span.light
- has_icon = provider_has_icon?(provider) - 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 @@ ...@@ -3,7 +3,7 @@
.col-md-4.col-lg-6 .col-md-4.col-lg-6
= users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true) = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true)
.help-block.append-bottom-10 .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 .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" = 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 @@ ...@@ -16,7 +16,7 @@
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
.help-block.append-bottom-10 .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 .col-md-2
= f.submit 'Add to group', class: "btn btn-create btn-block" = f.submit 'Add to group', class: "btn btn-create btn-block"
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- if can?(current_user, :admin_group_member, @group) - if can?(current_user, :admin_group_member, @group)
.project-members-new.append-bottom-default .project-members-new.append-bottom-default
%p.clearfix %p.clearfix
Add new user to Add new member to
%strong= @group.name %strong= @group.name
= render "new_group_member" = render "new_group_member"
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
.append-bottom-default.clearfix .append-bottom-default.clearfix
%h5.member.existing-title %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_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group .form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
= render 'shared/members/sort_dropdown' = render 'shared/members/sort_dropdown'
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Users with access to Members with access to
%strong= @group.name %strong= @group.name
%span.badge= @members.total_count %span.badge= @members.total_count
%ul.content-list %ul.content-list
......
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
rather than Git. Please convert rather than Git. Please convert
= link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview' = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview'
and go through the 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. again.
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } } .js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
...@@ -33,6 +33,8 @@ ...@@ -33,6 +33,8 @@
- if content_for?(:page_specific_javascripts) - if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts = yield :page_specific_javascripts
= yield :project_javascripts
= csrf_meta_tags = csrf_meta_tags
- unless browser.safari? - unless browser.safari?
......
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
%body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } %body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= Gon::Base.render_data = 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/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav = render 'layouts/page', sidebar: sidebar, nav: nav
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- header_title project_title(@project) unless header_title - header_title project_title(@project) unless header_title
- nav "project" - nav "project"
- content_for :scripts_body_top do - content_for :project_javascripts do
- project = @target_project || @project - project = @target_project || @project
- if @project_wiki && @page - if @project_wiki && @page
- preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug) - preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug)
......
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect Disconnect
- else - 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 Connect
%hr %hr
- if current_user.can_change_username? - if current_user.can_change_username?
......
...@@ -24,5 +24,10 @@ ...@@ -24,5 +24,10 @@
":list" => "list", ":list" => "list",
":disabled" => "disabled", ":disabled" => "disabled",
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
":key" => "_uid" } ":key" => "_uid" }
= render "projects/boards/components/sidebar" = 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 @@ ...@@ -29,6 +29,7 @@
":loading" => "list.loading", ":loading" => "list.loading",
":disabled" => "disabled", ":disabled" => "disabled",
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
"ref" => "board-list" } "ref" => "board-list" }
- if can?(current_user, :admin_list, @project) - if can?(current_user, :admin_list, @project)
= render "projects/boards/components/blank_state" = render "projects/boards/components/blank_state"
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
":list" => "list", ":list" => "list",
":issue" => "issue", ":issue" => "issue",
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
":disabled" => "disabled", ":disabled" => "disabled",
":key" => "issue.id" } ":key" => "issue.id" }
%li.board-list-count.text-center{ "v-if" => "showCount" } %li.board-list-count.text-center{ "v-if" => "showCount" }
......
...@@ -4,25 +4,7 @@ ...@@ -4,25 +4,7 @@
"@mousedown" => "mouseDown", "@mousedown" => "mouseDown",
"@mousemove" => "mouseMove", "@mousemove" => "mouseMove",
"@mouseup" => "showIssue($event)" } "@mouseup" => "showIssue($event)" }
%h4.card-title %issue-card-inner{ ":list" => "list",
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") ":issue" => "issue",
%a{ ":href" => 'issueLinkBase + "/" + issue.id', ":issue-link-base" => "issueLinkBase",
":title" => "issue.title" } ":root-path" => "rootPath" }
{{ 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 }}
...@@ -22,3 +22,5 @@ ...@@ -22,3 +22,5 @@
= render "projects/boards/components/sidebar/due_date" = render "projects/boards/components/sidebar/due_date"
= render "projects/boards/components/sidebar/labels" = render "projects/boards/components/sidebar/labels"
= render "projects/boards/components/sidebar/notifications" = render "projects/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue",
":list" => "list" }
...@@ -8,8 +8,12 @@ ...@@ -8,8 +8,12 @@
- last_line = right.new_pos if right - last_line = right.new_pos if right
%tr.line_holder.parallel %tr.line_holder.parallel
- if left - if left
- if left.meta? - case left.type
- when 'match'
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel = 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 - else
- left_line_code = diff_file.line_code(left) - left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left) - left_position = diff_file.position(left)
...@@ -21,8 +25,12 @@ ...@@ -21,8 +25,12 @@
%td.line_content.parallel %td.line_content.parallel
- if right - if right
- if right.meta? - case right.type
- when 'match'
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel = 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 - else
- right_line_code = diff_file.line_code(right) - right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right) - right_position = diff_file.position(right)
......
...@@ -109,10 +109,10 @@ ...@@ -109,10 +109,10 @@
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title
:javascript :javascript
var merge_request; $(function () {
new MergeRequest({
merge_request = new MergeRequest({
action: "#{controller.action_name}" action: "#{controller.action_name}"
}); });
});
var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}"; var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}";
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
= icon('github', text: 'GitHub') = icon('github', text: 'GitHub')
%div %div
- if bitbucket_import_enabled? - 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') = icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured? - unless bitbucket_import_configured?
= render 'bitbucket_import_modal' = render 'bitbucket_import_modal'
......
...@@ -38,8 +38,9 @@ ...@@ -38,8 +38,9 @@
#js-boards-search.issue-boards-search #js-boards-search.issue-boards-search
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project) - if can?(current_user, :admin_list, @project)
#js-add-issues-btn.pull-right.prepend-left-10
.dropdown.pull-right .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 Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable .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" } = 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 @@ ...@@ -91,5 +92,5 @@
new SubscriptionSelect(); new SubscriptionSelect();
$('form.filter-form').on('submit', function (event) { $('form.filter-form').on('submit', function (event) {
event.preventDefault(); 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 ...@@ -267,7 +267,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :boards, only: [:index, :show] do resources :boards, only: [:index, :show] do
scope module: :boards do scope module: :boards do
resources :issues, only: [:update] resources :issues, only: [:index, :update]
resources :lists, only: [:index, :create, :update, :destroy] do resources :lists, only: [:index, :create, :update, :destroy] do
collection 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. ...@@ -34,3 +34,4 @@ do.
| `/remove_estimate` | Remove estimated time | | `/remove_estimate` | Remove estimated time |
| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or substract spent time | | <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or substract spent time |
| `/remove_time_spent` | Remove time spent | | `/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 ...@@ -501,6 +501,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I fill in merge request search with "Fe"' do step 'I fill in merge request search with "Fe"' do
fill_in 'issuable_search', with: "Fe" fill_in 'issuable_search', with: "Fe"
page.within '.merge-requests-holder' do
find('.merge-request')
end
end end
step 'I click the "Target branch" dropdown' do step 'I click the "Target branch" dropdown' do
......
...@@ -37,7 +37,7 @@ module API ...@@ -37,7 +37,7 @@ module API
end end
desc 'Get the lists of a project board' do 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 success Entities::List
end end
get '/lists' do get '/lists' do
......
...@@ -18,9 +18,19 @@ describe Projects::Boards::IssuesController do ...@@ -18,9 +18,19 @@ describe Projects::Boards::IssuesController do
end end
describe 'GET index' do 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 context 'with valid list id' do
it 'returns issues that have the list label applied' 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]) issue = create(:labeled_issue, project: project, labels: [planning])
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) create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
...@@ -36,19 +46,29 @@ describe Projects::Boards::IssuesController do ...@@ -36,19 +46,29 @@ describe Projects::Boards::IssuesController do
end end
end end
context 'with invalid board id' do context 'with invalid list id' do
it 'returns a not found 404 response' 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) expect(response).to have_http_status(404)
end end
end end
end
context 'with invalid list id' do context 'when list id is missing' do
it 'returns a not found 404 response' do it 'returns opened issues without board labels applied' do
list_issues user: user, board: board, list: 999 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
end end
...@@ -65,13 +85,17 @@ describe Projects::Boards::IssuesController do ...@@ -65,13 +85,17 @@ describe Projects::Boards::IssuesController do
end end
end end
def list_issues(user:, board:, list:) def list_issues(user:, board:, list: nil)
sign_in(user) sign_in(user)
get :index, namespace_id: project.namespace.to_param, params = {
namespace_id: project.namespace.to_param,
project_id: project.to_param, project_id: project.to_param,
board_id: board.to_param, board_id: board.to_param,
list_id: list.to_param list_id: list.try(:to_param)
}
get :index, params.compact
end end
end end
......
...@@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do ...@@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do
parsed_response = JSON.parse(response.body) parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('lists') expect(response).to match_response_schema('lists')
expect(parsed_response.length).to eq 3 expect(parsed_response.length).to eq 2
end end
context 'with unauthorized user' do context 'with unauthorized user' do
......
...@@ -3,7 +3,6 @@ FactoryGirl.define do ...@@ -3,7 +3,6 @@ FactoryGirl.define do
project factory: :empty_project project factory: :empty_project
after(:create) do |board| after(:create) do |board|
board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done) board.lists.create(list_type: :done)
end end
end end
......
...@@ -3,6 +3,18 @@ FactoryGirl.define do ...@@ -3,6 +3,18 @@ FactoryGirl.define do
project factory: :empty_project project factory: :empty_project
author factory: :user 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 factory :closed_issue_event do
action { Event::CLOSED } action { Event::CLOSED }
target factory: :closed_issue target factory: :closed_issue
......
...@@ -6,12 +6,6 @@ FactoryGirl.define do ...@@ -6,12 +6,6 @@ FactoryGirl.define do
sequence(:position) sequence(:position)
end end
factory :backlog_list, parent: :list do
list_type :backlog
label nil
position nil
end
factory :done_list, parent: :list do factory :done_list, parent: :list do
list_type :done list_type :done
label nil 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 ...@@ -20,7 +20,7 @@ describe 'Issue Boards', feature: true, js: true do
before do before do
visit namespace_project_board_path(project.namespace, project, board) visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.board', count: 3) expect(page).to have_selector('.board', count: 2)
end end
it 'shows blank state' do it 'shows blank state' do
...@@ -31,18 +31,18 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -31,18 +31,18 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.board-blank-state')) do page.within(find('.board-blank-state')) do
click_button("Nevermind, I'll use my own") click_button("Nevermind, I'll use my own")
end end
expect(page).to have_selector('.board', count: 2) expect(page).to have_selector('.board', count: 1)
end end
it 'creates default lists' do it 'creates default lists' do
lists = ['Backlog', 'To Do', 'Doing', 'Done'] lists = ['To Do', 'Doing', 'Done']
page.within(find('.board-blank-state')) do page.within(find('.board-blank-state')) do
click_button('Add default lists') click_button('Add default lists')
end end
wait_for_vue_resource 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| page.all('.board').each_with_index do |list, i|
expect(list.find('.board-title')).to have_content(lists[i]) expect(list.find('.board-title')).to have_content(lists[i])
...@@ -64,42 +64,41 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -64,42 +64,41 @@ describe 'Issue Boards', feature: true, js: true do
let!(:list1) { create(:list, board: board, label: planning, position: 0) } let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: development, position: 1) } let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) }
let!(:issue1) { create(:issue, project: project, assignee: user) } let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) }
let!(:issue2) { create(:issue, project: project, author: user2) } let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) }
let!(:issue3) { create(:issue, project: project) } let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) }
let!(:issue4) { create(:issue, project: project) } let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) }
let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) } let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) } let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) } let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
let!(:issue8) { create(:closed_issue, project: project) } 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 before do
visit namespace_project_board_path(project.namespace, project, board) visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource 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(1)')).to have_selector('.card')
expect(find('.board:nth-child(2)')).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(3)')).to have_selector('.card')
expect(find('.board:nth-child(4)')).to have_selector('.card')
end end
it 'shows lists' do it 'shows lists' do
expect(page).to have_selector('.board', count: 4) expect(page).to have_selector('.board', count: 3)
end end
it 'shows description tooltip on list title' do 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') expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')
end end
end end
it 'shows issues in lists' do it 'shows issues in lists' do
wait_for_board_cards(1, 8)
wait_for_board_cards(2, 2) wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
end end
it 'shows confidential issues with icon' do it 'shows confidential issues with icon' do
...@@ -108,19 +107,6 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -108,19 +107,6 @@ describe 'Issue Boards', feature: true, js: true do
end end
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 it 'search done list' do
page.within('#js-boards-search') do page.within('#js-boards-search') do
find('.form-control').set(issue8.title) find('.form-control').set(issue8.title)
...@@ -130,8 +116,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -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(1)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(2)')).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(3)')).to have_selector('.card', count: 1)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
end end
it 'search list' do it 'search list' do
...@@ -141,157 +126,135 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -141,157 +126,135 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
expect(find('.board:nth-child(2)')).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(3)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
end end
it 'allows user to delete board' do 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 find('.board-delete').click
end end
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.board', count: 3) expect(page).to have_selector('.board', count: 2)
end end
it 'removes checkmark in new list dropdown after deleting' do it 'removes checkmark in new list dropdown after deleting' do
click_button 'Add list' click_button 'Add list'
wait_for_ajax wait_for_ajax
page.within(find('.board:nth-child(2)')) do page.within(find('.board:nth-child(1)')) do
find('.board-delete').click find('.board-delete').click
end end
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.board', count: 3) expect(page).to have_selector('.board', count: 2)
expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active')
end end
it 'infinite scrolls list' do it 'infinite scrolls list' do
50.times do 50.times do
create(:issue, project: project) create(:labeled_issue, project: project, labels: [planning])
end end
visit namespace_project_board_path(project.namespace, project, board) visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource wait_for_vue_resource
page.within(find('.board', match: :first)) do 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_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") evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.card', count: 40) 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") evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource 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') expect(page).to have_content('Showing all issues')
end end
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 context 'done' do
it 'shows list of done issues' do it 'shows list of done issues' do
wait_for_board_cards(4, 1) wait_for_board_cards(3, 1)
wait_for_ajax wait_for_ajax
end end
it 'moves issue to done' do 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(2, 2)
wait_for_board_cards(3, 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(1)')).not_to have_content(issue9.title)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2) expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2)
expect(find('.board:nth-child(4)')).to have_content(issue9.title) expect(find('.board:nth-child(3)')).to have_content(issue9.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title) expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end end
it 'removes all of the same issue to done' do 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(1, 7)
wait_for_board_cards(2, 1) wait_for_board_cards(2, 2)
wait_for_board_cards(3, 1) wait_for_board_cards(3, 2)
wait_for_board_cards(4, 2)
expect(find('.board:nth-child(2)')).not_to have_content(issue6.title) expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
expect(find('.board:nth-child(4)')).to have_content(issue6.title) expect(find('.board:nth-child(3)')).to have_content(issue9.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title) expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end end
end end
context 'lists' do context 'lists' do
it 'changes position of list' 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(1, 2)
wait_for_board_cards(2, 2) wait_for_board_cards(2, 8)
wait_for_board_cards(3, 2) wait_for_board_cards(3, 1)
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(development.title) expect(find('.board:nth-child(1)')).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(planning.title)
end end
it 'issue moves between lists' do 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(1, 7)
wait_for_board_cards(2, 1) wait_for_board_cards(2, 2)
wait_for_board_cards(3, 3) wait_for_board_cards(3, 1)
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(3)')).to have_content(issue6.title) expect(find('.board:nth-child(2)')).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)').all('.card').last).not_to have_content(development.title)
end end
it 'issue moves between lists' do 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(1, 9)
wait_for_board_cards(2, 3) wait_for_board_cards(2, 1)
wait_for_board_cards(3, 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(1)')).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)').all('.card').first).not_to have_content(planning.title)
end end
it 'issue moves from done' do 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) 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(2, 3)
wait_for_board_cards(3, 2) wait_for_board_cards(3, 0)
wait_for_board_cards(4, 0)
end end
context 'issue card' do context 'issue card' do
...@@ -324,7 +287,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -324,7 +287,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.board', count: 5) expect(page).to have_selector('.board', count: 4)
end end
it 'creates new list for Backlog label' do it 'creates new list for Backlog label' do
...@@ -337,7 +300,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -337,7 +300,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.board', count: 5) expect(page).to have_selector('.board', count: 4)
end end
it 'creates new list for Done label' do it 'creates new list for Done label' do
...@@ -350,7 +313,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -350,7 +313,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.board', count: 5) expect(page).to have_selector('.board', count: 4)
end end
it 'keeps dropdown open after adding new list' do it 'keeps dropdown open after adding new list' do
...@@ -366,21 +329,6 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -366,21 +329,6 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.issue-boards-search')).to have_selector('.open') expect(find('.issue-boards-search')).to have_selector('.open')
end 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 it 'creates new list from a new label' do
click_button 'Add list' click_button 'Add list'
...@@ -397,7 +345,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -397,7 +345,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax wait_for_ajax
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.board', count: 5) expect(page).to have_selector('.board', count: 4)
end end
end end
end end
...@@ -418,7 +366,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -418,7 +366,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
wait_for_board_cards(1, 1) wait_for_board_cards(1, 1)
wait_for_empty_boards((2..4)) wait_for_empty_boards((2..3))
end end
it 'filters by assignee' do it 'filters by assignee' do
...@@ -437,7 +385,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -437,7 +385,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
wait_for_board_cards(1, 1) wait_for_board_cards(1, 1)
wait_for_empty_boards((2..4)) wait_for_empty_boards((2..3))
end end
it 'filters by milestone' do it 'filters by milestone' do
...@@ -454,10 +402,9 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -454,10 +402,9 @@ describe 'Issue Boards', feature: true, js: true do
end end
wait_for_vue_resource wait_for_vue_resource
wait_for_board_cards(1, 0) wait_for_board_cards(1, 1)
wait_for_board_cards(2, 1) wait_for_board_cards(2, 0)
wait_for_board_cards(3, 0) wait_for_board_cards(3, 0)
wait_for_board_cards(4, 0)
end end
it 'filters by label' do it 'filters by label' do
...@@ -474,7 +421,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -474,7 +421,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
wait_for_board_cards(1, 1) wait_for_board_cards(1, 1)
wait_for_empty_boards((2..4)) wait_for_empty_boards((2..3))
end end
it 'filters by label with space after reload' do it 'filters by label with space after reload' do
...@@ -530,7 +477,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -530,7 +477,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'infinite scrolls list with label filter' do it 'infinite scrolls list with label filter' do
50.times do 50.times do
create(:labeled_issue, project: project, labels: [testing]) create(:labeled_issue, project: project, labels: [planning, testing])
end end
page.within '.issues-filters' do page.within '.issues-filters' do
...@@ -580,32 +527,12 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -580,32 +527,12 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
wait_for_board_cards(1, 1) wait_for_board_cards(1, 1)
wait_for_empty_boards((2..4)) wait_for_empty_boards((2..3))
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)
end end
it 'filters by clicking label button on issue' do it 'filters by clicking label button on issue' do
page.within(find('.board', match: :first)) 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) expect(find('.card', match: :first)).to have_content(bug.title)
click_button(bug.title) click_button(bug.title)
wait_for_vue_resource wait_for_vue_resource
...@@ -614,7 +541,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -614,7 +541,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
wait_for_board_cards(1, 1) wait_for_board_cards(1, 1)
wait_for_empty_boards((2..4)) wait_for_empty_boards((2..3))
page.within('.labels-filter') do page.within('.labels-filter') do
expect(find('.dropdown-toggle-text')).to have_content(bug.title) expect(find('.dropdown-toggle-text')).to have_content(bug.title)
......
...@@ -6,6 +6,7 @@ describe 'Issue Boards new issue', feature: true, js: true do ...@@ -6,6 +6,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) } let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, position: 0) }
let(:user) { create(:user) } let(:user) { create(:user) }
context 'authorized user' do context 'authorized user' do
...@@ -17,7 +18,7 @@ describe 'Issue Boards new issue', feature: true, js: true do ...@@ -17,7 +18,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
visit namespace_project_board_path(project.namespace, project, board) visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource wait_for_vue_resource
expect(page).to have_selector('.board', count: 3) expect(page).to have_selector('.board', count: 2)
end end
it 'displays new issue button' do it 'displays new issue button' do
...@@ -25,7 +26,7 @@ describe 'Issue Boards new issue', feature: true, js: true do ...@@ -25,7 +26,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
end end
it 'does not display new issue button in done list' do 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') expect(page).not_to have_selector('.board-issue-count-holder .btn')
end end
end end
......
...@@ -4,14 +4,17 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -4,14 +4,17 @@ describe 'Issue Boards', feature: true, js: true do
include WaitForAjax include WaitForAjax
include WaitForVueResource include WaitForVueResource
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:label) { create(:label, project: project) } let(:project) { create(:empty_project, :public) }
let!(:label2) { create(:label, project: project) }
let!(:milestone) { create(:milestone, project: project) } let!(:milestone) { create(:milestone, project: project) }
let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) } let!(:development) { create(:label, project: project, name: 'Development') }
let!(:issue) { create(:issue, project: project) } 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 before do
project.team << [user, :master] project.team << [user, :master]
...@@ -62,8 +65,22 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -62,8 +65,22 @@ describe 'Issue Boards', feature: true, js: true do
end end
page.within('.issue-boards-sidebar') do page.within('.issue-boards-sidebar') do
expect(page).to have_content(issue.title) expect(page).to have_content(issue2.title)
expect(page).to have_content(issue.to_reference) 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
end end
...@@ -244,22 +261,22 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -244,22 +261,22 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax wait_for_ajax
click_link label.title click_link bug.title
wait_for_vue_resource wait_for_vue_resource
find('.dropdown-menu-close-icon').click find('.dropdown-menu-close-icon').click
page.within('.value') do page.within('.value') do
expect(page).to have_selector('.label', count: 1) expect(page).to have_selector('.label', count: 3)
expect(page).to have_content(label.title) expect(page).to have_content(bug.title)
end end
end end
page.within(first('.board')) do page.within(first('.board')) do
page.within(first('.card')) do page.within(first('.card')) do
expect(page).to have_selector('.label', count: 1) expect(page).to have_selector('.label', count: 2)
expect(page).to have_content(label.title) expect(page).to have_content(bug.title)
end end
end end
end end
...@@ -274,32 +291,32 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -274,32 +291,32 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax wait_for_ajax
click_link label.title click_link bug.title
click_link label2.title click_link regression.title
wait_for_vue_resource wait_for_vue_resource
find('.dropdown-menu-close-icon').click find('.dropdown-menu-close-icon').click
page.within('.value') do page.within('.value') do
expect(page).to have_selector('.label', count: 2) expect(page).to have_selector('.label', count: 4)
expect(page).to have_content(label.title) expect(page).to have_content(bug.title)
expect(page).to have_content(label2.title) expect(page).to have_content(regression.title)
end end
end end
page.within(first('.board')) do page.within(first('.board')) do
page.within(first('.card')) do page.within(first('.card')) do
expect(page).to have_selector('.label', count: 2) expect(page).to have_selector('.label', count: 3)
expect(page).to have_content(label.title) expect(page).to have_content(bug.title)
expect(page).to have_content(label2.title) expect(page).to have_content(regression.title)
end end
end end
end end
it 'removes a label' do it 'removes a label' do
page.within(first('.board')) do page.within(first('.board')) do
find('.card:nth-child(2)').click first('.card').click
end end
page.within('.labels') do page.within('.labels') do
...@@ -307,22 +324,22 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -307,22 +324,22 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax wait_for_ajax
click_link label.title click_link stretch.title
wait_for_vue_resource wait_for_vue_resource
find('.dropdown-menu-close-icon').click find('.dropdown-menu-close-icon').click
page.within('.value') do page.within('.value') do
expect(page).to have_selector('.label', count: 0) expect(page).to have_selector('.label', count: 1)
expect(page).not_to have_content(label.title) expect(page).not_to have_content(stretch.title)
end end
end end
page.within(first('.board')) do page.within(first('.board')) do
page.within(find('.card:nth-child(2)')) do page.within(first('.card')) do
expect(page).not_to have_selector('.label', count: 1) expect(page).not_to have_selector('.label')
expect(page).not_to have_content(label.title) expect(page).not_to have_content(stretch.title)
end end
end end
end end
......
...@@ -120,5 +120,79 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do ...@@ -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' expect(page).not_to have_content '/due 2016-08-28'
end end
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
end end
...@@ -10,15 +10,12 @@ describe ContributedProjectsFinder do ...@@ -10,15 +10,12 @@ describe ContributedProjectsFinder do
let!(:private_project) { create(:empty_project, :private) } let!(:private_project) { create(:empty_project, :private) }
before do before do
private_project.team << [source_user, Gitlab::Access::MASTER] private_project.add_master(source_user)
private_project.team << [current_user, Gitlab::Access::DEVELOPER] private_project.add_developer(current_user)
public_project.team << [source_user, Gitlab::Access::MASTER] public_project.add_master(source_user)
create(:event, action: Event::PUSHED, project: public_project, create(:event, :pushed, project: public_project, target: public_project, author: source_user)
target: public_project, author: source_user) create(:event, :pushed, project: private_project, target: private_project, author: source_user)
create(:event, action: Event::PUSHED, project: private_project,
target: private_project, author: source_user)
end end
describe 'without a current user' do describe 'without a current user' do
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
"confidential" "confidential"
], ],
"properties" : { "properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" }, "iid": { "type": "integer" },
"title": { "type": "string" }, "title": { "type": "string" },
"confidential": { "type": "boolean" }, "confidential": { "type": "boolean" },
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
"id": { "type": "integer" }, "id": { "type": "integer" },
"list_type": { "list_type": {
"type": "string", "type": "string",
"enum": ["backlog", "label", "done"] "enum": ["label", "done"]
}, },
"label": { "label": {
"type": ["object", "null"], "type": ["object", "null"],
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
}); });
}); });
return load = function() { return load = function() {
return $(document).trigger('page:load'); return $(document).trigger('load');
}; };
}); });
}).call(this); }).call(this);
...@@ -34,11 +34,5 @@ ...@@ -34,11 +34,5 @@
$('#required5').val('1').change(); $('#required5').val('1').change();
return expect($('.submit')).not.toBeDisabled(); 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); }).call(this);
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
describe('Store', () => { describe('Store', () => {
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor); 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(); gl.issueBoards.BoardsStore.create();
Cookies.set('issue_board_welcome_hidden', 'false', { Cookies.set('issue_board_welcome_hidden', 'false', {
...@@ -61,18 +61,6 @@ describe('Store', () => { ...@@ -61,18 +61,6 @@ describe('Store', () => {
expect(list).toBeDefined(); 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) => { it('gets issue when new list added', (done) => {
gl.issueBoards.BoardsStore.addList(listObj); gl.issueBoards.BoardsStore.addList(listObj);
const list = gl.issueBoards.BoardsStore.findList('id', 1); const list = gl.issueBoards.BoardsStore.findList('id', 1);
...@@ -117,10 +105,7 @@ describe('Store', () => { ...@@ -117,10 +105,7 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false); expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
}); });
it('check for blank state adding when backlog & done list exist', () => { it('check for blank state adding when done list exist', () => {
gl.issueBoards.BoardsStore.addList({
list_type: 'backlog'
});
gl.issueBoards.BoardsStore.addList({ gl.issueBoards.BoardsStore.addList({
list_type: 'done' 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', () => { ...@@ -20,7 +20,7 @@ describe('Issue model', () => {
let issue; let issue;
beforeEach(() => { beforeEach(() => {
gl.boardService = new BoardService('/test/issue-boards/board', '1'); gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create(); gl.issueBoards.BoardsStore.create();
issue = new ListIssue({ issue = new ListIssue({
......
...@@ -24,7 +24,7 @@ describe('List model', () => { ...@@ -24,7 +24,7 @@ describe('List model', () => {
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor); 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(); gl.issueBoards.BoardsStore.create();
list = new List(listObj); 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 @@ ...@@ -50,7 +50,6 @@
secondTab.click(); secondTab.click();
expect(historySpy).toHaveBeenCalledWith({ expect(historySpy).toHaveBeenCalledWith({
turbolinks: true,
url: newState, url: newState,
}, document.title, newState); }, document.title, newState);
}); });
......
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global Build */ /* global Build */
/* global Turbolinks */
//= require lib/utils/datetime_utility //= require lib/utils/datetime_utility
//= require lib/utils/url_utility
//= require build //= require build
//= require breakpoints //= require breakpoints
//= require jquery.nicescroll //= require jquery.nicescroll
//= require turbolinks
describe('Build', () => { describe('Build', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
...@@ -167,7 +166,7 @@ describe('Build', () => { ...@@ -167,7 +166,7 @@ describe('Build', () => {
}); });
it('reloads the page when the build is done', () => { it('reloads the page when the build is done', () => {
spyOn(Turbolinks, 'visit'); spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001); jasmine.clock().tick(4001);
const [{ success, context }] = $.ajax.calls.argsFor(1); const [{ success, context }] = $.ajax.calls.argsFor(1);
...@@ -177,7 +176,7 @@ describe('Build', () => { ...@@ -177,7 +176,7 @@ describe('Build', () => {
append: true, append: true,
}); });
expect(Turbolinks.visit).toHaveBeenCalledWith(BUILD_URL); expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
}); });
}); });
}); });
......
/* global Turbolinks */ //= require lib/utils/url_utility
//= require turbolinks
//= require lib/utils/common_utils //= require lib/utils/common_utils
//= require filtered_search/filtered_search_token_keys //= require filtered_search/filtered_search_token_keys
//= require filtered_search/filtered_search_tokenizer //= require filtered_search/filtered_search_tokenizer
...@@ -38,7 +36,7 @@ ...@@ -38,7 +36,7 @@
it('should search with a single word', () => { it('should search with a single word', () => {
getInput().value = 'searchTerm'; getInput().value = 'searchTerm';
spyOn(Turbolinks, 'visit').and.callFake((url) => { spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`); expect(url).toEqual(`${defaultParams}&search=searchTerm`);
}); });
...@@ -48,7 +46,7 @@ ...@@ -48,7 +46,7 @@
it('should search with multiple words', () => { it('should search with multiple words', () => {
getInput().value = 'awesome search terms'; 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`); expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
}); });
...@@ -58,7 +56,7 @@ ...@@ -58,7 +56,7 @@
it('should search with special characters', () => { it('should search with special characters', () => {
getInput().value = '~!@#$%^&*()_+{}:<>,.?/'; 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`); 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 */ /* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
/* global Turbolinks */
/*= require jquery */ /*= require jquery */
/*= require gl_dropdown */ /*= require gl_dropdown */
/*= require turbolinks */
/*= require lib/utils/common_utils */ /*= require lib/utils/common_utils */
/*= require lib/utils/type_utility */ /*= require lib/utils/type_utility */
//= require lib/utils/url_utility
(() => { (() => {
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
...@@ -113,13 +112,13 @@ ...@@ -113,13 +112,13 @@
expect(this.dropdownContainerElement).toHaveClass('open'); expect(this.dropdownContainerElement).toHaveClass('open');
const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => { navigateWithKeys('down', randomIndex, () => {
spyOn(Turbolinks, 'visit').and.stub(); spyOn(gl.utils, 'visitUrl').and.stub();
navigateWithKeys('enter', null, () => { navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open'); expect(this.dropdownContainerElement).not.toHaveClass('open');
const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active'); expect(link).toHaveClass('is-active');
const linkedLocation = link.attr('href'); 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 Issuable */
/* global Turbolinks */
//= require lib/utils/url_utility
//= require issuable //= require issuable
//= require turbolinks
(() => { (() => {
const BASE_URL = '/user/project/issues?scope=all&state=closed'; const BASE_URL = '/user/project/issues?scope=all&state=closed';
...@@ -42,39 +41,39 @@ ...@@ -42,39 +41,39 @@
}); });
it('should contain only the default parameters', () => { it('should contain only the default parameters', () => {
spyOn(Turbolinks, 'visit'); spyOn(gl.utils, 'visitUrl');
Issuable.filterResults($filtersForm); 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"', () => { it('should filter for the phrase "broken"', () => {
spyOn(Turbolinks, 'visit'); spyOn(gl.utils, 'visitUrl');
updateForm({ search: 'broken' }, $filtersForm); updateForm({ search: 'broken' }, $filtersForm);
Issuable.filterResults($filtersForm); Issuable.filterResults($filtersForm);
const params = `${DEFAULT_PARAMS}&search=broken`; 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', () => { it('should keep query parameters after modifying filter', () => {
spyOn(Turbolinks, 'visit'); spyOn(gl.utils, 'visitUrl');
// initial filter // initial filter
updateForm({ milestone_title: 'v1.0' }, $filtersForm); updateForm({ milestone_title: 'v1.0' }, $filtersForm);
Issuable.filterResults($filtersForm); Issuable.filterResults($filtersForm);
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; 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 // update filter
updateForm({ label_name: 'Frontend' }, $filtersForm); updateForm({ label_name: 'Frontend' }, $filtersForm);
Issuable.filterResults($filtersForm); Issuable.filterResults($filtersForm);
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; 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 @@ ...@@ -21,5 +21,19 @@
expect(largeFont > regular).toBe(true); 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 @@ ...@@ -99,7 +99,6 @@
}); });
newState = this.subject('commits'); newState = this.subject('commits');
expect(this.spies.history).toHaveBeenCalledWith({ expect(this.spies.history).toHaveBeenCalledWith({
turbolinks: true,
url: newState url: newState
}, document.title, newState); }, document.title, newState);
}); });
......
...@@ -6,8 +6,6 @@ ...@@ -6,8 +6,6 @@
/*= require lib/utils/common_utils */ /*= require lib/utils/common_utils */
/*= require lib/utils/type_utility */ /*= require lib/utils/type_utility */
/*= require fuzzaldrin-plus */ /*= require fuzzaldrin-plus */
/*= require turbolinks */
/*= require jquery.turbolinks */
(function() { (function() {
var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
......
...@@ -164,7 +164,7 @@ ...@@ -164,7 +164,7 @@
const interval = this.smartInterval; const interval = this.smartInterval;
setTimeout(() => { setTimeout(() => {
$(document).trigger('page:before-unload'); $(document).trigger('beforeunload');
expect(interval.state.intervalId).toBeUndefined(); expect(interval.state.intervalId).toBeUndefined();
expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval); expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
done(); done();
......
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
// everything in application, however you may get better load performance if you // 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 the specific files that are being used in the spec that tests them.
/*= require jquery */ /*= require jquery */
/*= require jquery.turbolinks */
/*= require bootstrap */ /*= require bootstrap */
/*= require underscore */ /*= require underscore */
......
...@@ -5,15 +5,15 @@ describe EventFilter, lib: true do ...@@ -5,15 +5,15 @@ describe EventFilter, lib: true do
let(:source_user) { create(:user) } let(:source_user) { create(:user) }
let!(:public_project) { create(:empty_project, :public) } 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!(:push_event) { create(: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!(:merged_event) { create(: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!(:created_event) { create(: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!(:updated_event) { create(: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!(:closed_event) { create(: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!(:reopened_event) { create(: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!(:comments_event) { create(: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!(:joined_event) { create(: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!(:left_event) { create(:event, :left, project: public_project, target: public_project, author: source_user) }
it 'applies push filter' do it 'applies push filter' do
events = EventFilter.new(EventFilter.push).apply_filter(Event.all) events = EventFilter.new(EventFilter.push).apply_filter(Event.all)
......
...@@ -182,7 +182,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ...@@ -182,7 +182,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
project: project, project: project,
commit_id: ci_pipeline.sha) 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') create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
......
...@@ -19,7 +19,7 @@ describe Event, models: true do ...@@ -19,7 +19,7 @@ describe Event, models: true do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
it 'calls the reset_project_activity method' do 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) create_event(project, project.owner)
end end
...@@ -43,33 +43,33 @@ describe Event, models: true do ...@@ -43,33 +43,33 @@ describe Event, models: true do
describe '#membership_changed?' do describe '#membership_changed?' do
context "created" do context "created" do
subject { build(:event, action: Event::CREATED).membership_changed? } subject { build(:event, :created).membership_changed? }
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context "updated" do context "updated" do
subject { build(:event, action: Event::UPDATED).membership_changed? } subject { build(:event, :updated).membership_changed? }
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context "expired" do context "expired" do
subject { build(:event, action: Event::EXPIRED).membership_changed? } subject { build(:event, :expired).membership_changed? }
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
context "left" do context "left" do
subject { build(:event, action: Event::LEFT).membership_changed? } subject { build(:event, :left).membership_changed? }
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
context "joined" do context "joined" do
subject { build(:event, action: Event::JOINED).membership_changed? } subject { build(:event, :joined).membership_changed? }
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
end end
describe '#note?' do 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 context 'issue note event' do
let(:target) { create(:note_on_issue) } let(:target) { create(:note_on_issue) }
...@@ -97,7 +97,7 @@ describe Event, models: true do ...@@ -97,7 +97,7 @@ describe Event, models: true do
let(:note_on_commit) { create(:note_on_commit, project: project) } 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_issue) { create(:note_on_issue, noteable: issue, project: project) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_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 before do
project.team << [member, :developer] project.team << [member, :developer]
...@@ -221,13 +221,13 @@ describe Event, models: true do ...@@ -221,13 +221,13 @@ describe Event, models: true do
let!(:event2) { create(:closed_issue_event) } let!(:event2) { create(:closed_issue_event) }
describe 'without an explicit limit' do describe 'without an explicit limit' do
subject { Event.limit_recent } subject { described_class.limit_recent }
it { is_expected.to eq([event2, event1]) } it { is_expected.to eq([event2, event1]) }
end end
describe 'with an explicit limit' do describe 'with an explicit limit' do
subject { Event.limit_recent(1) } subject { described_class.limit_recent(1) }
it { is_expected.to eq([event2]) } it { is_expected.to eq([event2]) }
end end
...@@ -294,9 +294,9 @@ describe Event, models: true do ...@@ -294,9 +294,9 @@ describe Event, models: true do
} }
} }
Event.create({ described_class.create({
project: project, project: project,
action: Event::PUSHED, action: described_class::PUSHED,
data: data, data: data,
author_id: user.id author_id: user.id
}.merge!(attrs)) }.merge!(attrs))
......
...@@ -19,13 +19,6 @@ describe List do ...@@ -19,13 +19,6 @@ describe List do
expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id) expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
end 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 context 'when list_type is set to done' do
subject { described_class.new(list_type: :done) } subject { described_class.new(list_type: :done) }
...@@ -41,12 +34,6 @@ describe List do ...@@ -41,12 +34,6 @@ describe List do
expect(subject.destroy).to be_truthy expect(subject.destroy).to be_truthy
end 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 it 'can not be destroyed when when list_type is set to done' do
subject = create(:done_list) subject = create(:done_list)
...@@ -55,19 +42,13 @@ describe List do ...@@ -55,19 +42,13 @@ describe List do
end end
describe '#destroyable?' do 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 subject.list_type = :label
expect(subject).to be_destroyable expect(subject).to be_destroyable
end end
it 'retruns false when list_type is set to backlog' do it 'returns false when list_type is set to done' do
subject.list_type = :backlog
expect(subject).not_to be_destroyable
end
it 'retruns false when list_type is set to done' do
subject.list_type = :done subject.list_type = :done
expect(subject).not_to be_destroyable expect(subject).not_to be_destroyable
...@@ -75,19 +56,13 @@ describe List do ...@@ -75,19 +56,13 @@ describe List do
end end
describe '#movable?' do 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 subject.list_type = :label
expect(subject).to be_movable expect(subject).to be_movable
end end
it 'retruns false when list_type is set to backlog' do it 'returns false when list_type is set to done' do
subject.list_type = :backlog
expect(subject).not_to be_movable
end
it 'retruns false when list_type is set to done' do
subject.list_type = :done subject.list_type = :done
expect(subject).not_to be_movable expect(subject).not_to be_movable
...@@ -102,12 +77,6 @@ describe List do ...@@ -102,12 +77,6 @@ describe List do
expect(subject.title).to eq 'Development' expect(subject.title).to eq 'Development'
end 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 it 'returns Done when list_type is set to done' do
subject.list_type = :done subject.list_type = :done
......
...@@ -1013,8 +1013,8 @@ describe User, models: true do ...@@ -1013,8 +1013,8 @@ describe User, models: true do
let!(:project2) { create(:empty_project, forked_from_project: project3) } let!(:project2) { create(:empty_project, forked_from_project: project3) }
let!(:project3) { create(:empty_project) } let!(:project3) { create(:empty_project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) } 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!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) }
let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) } let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
before do before do
project1.team << [subject, :master] project1.team << [subject, :master]
...@@ -1058,7 +1058,7 @@ describe User, models: true do ...@@ -1058,7 +1058,7 @@ describe User, models: true do
let!(:push_data) do let!(:push_data) do
Gitlab::DataBuilder::Push.build_sample(project2, subject) Gitlab::DataBuilder::Push.build_sample(project2, subject)
end 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 before do
project1.team << [subject, :master] project1.team << [subject, :master]
...@@ -1086,7 +1086,7 @@ describe User, models: true do ...@@ -1086,7 +1086,7 @@ describe User, models: true do
expect(subject.recent_push(project2)).to eq(push_event) expect(subject.recent_push(project2)).to eq(push_event)
push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject) 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 expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
end end
......
...@@ -11,12 +11,11 @@ describe Boards::CreateService, services: true do ...@@ -11,12 +11,11 @@ describe Boards::CreateService, services: true do
expect { service.execute }.to change(Board, :count).by(1) expect { service.execute }.to change(Board, :count).by(1)
end end
it 'creates default lists' do it 'creates the default lists' do
board = service.execute board = service.execute
expect(board.lists.size).to eq 2 expect(board.lists.size).to eq 1
expect(board.lists.first).to be_backlog expect(board.lists.first).to be_done
expect(board.lists.last).to be_done
end end
end end
......
...@@ -13,7 +13,6 @@ describe Boards::Issues::ListService, services: true do ...@@ -13,7 +13,6 @@ describe Boards::Issues::ListService, services: true do
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } 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!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) } let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board) } let!(:done) { create(:done_list, board: board) }
...@@ -45,8 +44,8 @@ describe Boards::Issues::ListService, services: true do ...@@ -45,8 +44,8 @@ describe Boards::Issues::ListService, services: true do
end end
context 'sets default order to priority' do context 'sets default order to priority' do
it 'returns opened issues when listing issues from Backlog' do it 'returns opened issues when list id is missing' do
params = { board_id: board.id, id: backlog.id } params = { board_id: board.id }
issues = described_class.new(project, user, params).execute issues = described_class.new(project, user, params).execute
......
...@@ -10,7 +10,6 @@ describe Boards::Issues::MoveService, services: true do ...@@ -10,7 +10,6 @@ describe Boards::Issues::MoveService, services: true do
let(:development) { create(:label, project: project, name: 'Development') } let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') } 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!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) } let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board1) } let!(:done) { create(:done_list, board: board1) }
...@@ -19,41 +18,6 @@ describe Boards::Issues::MoveService, services: true do ...@@ -19,41 +18,6 @@ describe Boards::Issues::MoveService, services: true do
project.team << [user, :developer] project.team << [user, :developer]
end 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 context 'when moving an issue between lists' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } 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 } } 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 ...@@ -113,19 +77,6 @@ describe Boards::Issues::MoveService, services: true do
end end
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 context 'when moving to same list' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } 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 } } 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 ...@@ -21,7 +21,7 @@ describe Boards::Lists::CreateService, services: true do
end end
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 it 'creates a new list at beginning of the list' do
list = service.execute(board) list = service.execute(board)
...@@ -40,7 +40,7 @@ describe Boards::Lists::CreateService, services: true do ...@@ -40,7 +40,7 @@ describe Boards::Lists::CreateService, services: true do
end end
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 it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0) list1 = create(:list, board: board, position: 0)
......
...@@ -15,7 +15,6 @@ describe Boards::Lists::DestroyService, services: true do ...@@ -15,7 +15,6 @@ describe Boards::Lists::DestroyService, services: true do
end end
it 'decrements position of higher lists' do it 'decrements position of higher lists' do
backlog = board.backlog_list
development = create(:list, board: board, position: 0) development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1) review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2) staging = create(:list, board: board, position: 2)
...@@ -23,20 +22,12 @@ describe Boards::Lists::DestroyService, services: true do ...@@ -23,20 +22,12 @@ describe Boards::Lists::DestroyService, services: true do
described_class.new(project, user).execute(development) described_class.new(project, user).execute(development)
expect(backlog.reload.position).to be_nil
expect(review.reload.position).to eq 0 expect(review.reload.position).to eq 0
expect(staging.reload.position).to eq 1 expect(staging.reload.position).to eq 1
expect(done.reload.position).to be_nil expect(done.reload.position).to be_nil
end end
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 it 'does not remove list from board when list type is done' do
list = board.done_list list = board.done_list
service = described_class.new(project, user) service = described_class.new(project, user)
......
...@@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do ...@@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do
service = described_class.new(project, double) 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 end
end end
...@@ -6,7 +6,6 @@ describe Boards::Lists::MoveService, services: true do ...@@ -6,7 +6,6 @@ describe Boards::Lists::MoveService, services: true do
let(:board) { create(:board, project: project) } let(:board) { create(:board, project: project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:backlog) { create(:backlog_list, board: board) }
let!(:planning) { create(:list, board: board, position: 0) } let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) } let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) } let!(:review) { create(:list, board: board, position: 2) }
...@@ -87,14 +86,6 @@ describe Boards::Lists::MoveService, services: true do ...@@ -87,14 +86,6 @@ describe Boards::Lists::MoveService, services: true do
end end
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 it 'keeps position of lists when list type is done' do
service = described_class.new(project, user, position: 2) service = described_class.new(project, user, position: 2)
......
...@@ -653,5 +653,37 @@ describe SlashCommands::InterpretService, services: true do ...@@ -653,5 +653,37 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue } let(:issuable) { issue }
end end
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
end end
...@@ -35,7 +35,7 @@ module ExportFileHelper ...@@ -35,7 +35,7 @@ module ExportFileHelper
project: project, project: project,
commit_id: ci_pipeline.sha) 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(:project_member, :master, user: user, project: project)
create(:ci_variable, project: project) create(:ci_variable, project: project)
create(:ci_trigger, 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