Commit 3213dfd7 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'add-issues-to-boards' into 'master'

Add issues to boards list

Closes #26205

See merge request !8737
parents 0dc36591 a810e2e2
...@@ -13,11 +13,13 @@ ...@@ -13,11 +13,13 @@
//= require ./components/board //= require ./components/board
//= require ./components/board_sidebar //= require ./components/board_sidebar
//= require ./components/new_list_dropdown //= require ./components/new_list_dropdown
//= require ./components/modal/index
//= require ./vue_resource_interceptor //= require ./vue_resource_interceptor
$(() => { $(() => {
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();
})();
...@@ -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)) + '...';
}; };
......
...@@ -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;
} }
......
...@@ -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%;
}
...@@ -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
...@@ -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
......
...@@ -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" }
...@@ -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" }
......
...@@ -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
...@@ -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,23 +18,7 @@ describe Projects::Boards::IssuesController do ...@@ -18,23 +18,7 @@ describe Projects::Boards::IssuesController do
end end
describe 'GET index' do describe 'GET index' do
context 'with valid list id' do let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
it 'returns issues that have the list label applied' do
johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
issue = create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
issue.subscribe(johndoe, project)
list_issues user: user, board: board, list: list2
parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('issues')
expect(parsed_response.length).to eq 2
end
end
context 'with invalid board id' do context 'with invalid board id' do
it 'returns a not found 404 response' do it 'returns a not found 404 response' do
...@@ -44,11 +28,47 @@ describe Projects::Boards::IssuesController do ...@@ -44,11 +28,47 @@ describe Projects::Boards::IssuesController do
end end
end end
context 'with invalid list id' do context 'when list id is present' do
it 'returns a not found 404 response' do context 'with valid list id' do
list_issues user: user, board: board, list: 999 it 'returns issues that have the list label applied' do
issue = create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
issue.subscribe(johndoe, project)
expect(response).to have_http_status(404) list_issues user: user, board: board, list: list2
parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('issues')
expect(parsed_response.length).to eq 2
end
end
context 'with invalid list id' do
it 'returns a not found 404 response' do
list_issues user: user, board: board, list: 999
expect(response).to have_http_status(404)
end
end
end
context 'when list id is missing' do
it 'returns opened issues without board labels applied' do
bug = create(:label, project: project, name: 'Bug')
create(:issue, project: project)
create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [development])
create(:labeled_issue, project: project, labels: [bug])
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 = {
project_id: project.to_param, namespace_id: project.namespace.to_param,
board_id: board.to_param, project_id: project.to_param,
list_id: list.to_param board_id: board.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
......
...@@ -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
This diff is collapsed.
...@@ -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(:user) { create(:user) }
let(:board) { create(:board, project: project) } let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) } let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) } let!(:development) { create(:label, project: project, name: 'Development') }
let!(:label2) { create(:label, project: project) } let!(:bug) { create(:label, project: project, name: 'Bug') }
let!(:milestone) { create(:milestone, project: project) } let!(:regression) { create(:label, project: project, name: 'Regression') }
let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) } let!(:stretch) { create(:label, project: project, name: 'Stretch') }
let!(:issue) { create(:issue, project: project) } 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
......
...@@ -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"],
......
...@@ -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);
});
});
...@@ -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');
});
});
}); });
})(); })();
...@@ -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
......
...@@ -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)
......
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