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');
} }
$(document) if (!isBound) {
.off('click', '.js-unfold, .diff-line-num a') $(document)
.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,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
......
...@@ -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
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
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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