summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es613
-rw-r--r--app/assets/javascripts/boards/components/board.js.es621
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es634
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es67
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es651
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js.es64
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es62
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es623
-rw-r--r--app/assets/javascripts/boards/models/milestone.js.es66
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es62
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es63
-rw-r--r--app/assets/javascripts/due_date_select.js.es629
-rw-r--r--app/assets/javascripts/gl_dropdown.js11
-rw-r--r--app/assets/javascripts/labels_select.js29
-rw-r--r--app/assets/javascripts/milestone_select.js21
-rw-r--r--app/assets/javascripts/right_sidebar.js11
-rw-r--r--app/assets/javascripts/subscription.js32
-rw-r--r--app/assets/javascripts/users_select.js45
-rw-r--r--app/assets/stylesheets/pages/boards.scss59
-rw-r--r--app/controllers/projects/boards/issues_controller.rb9
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/views/projects/boards/components/_card.html.haml7
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml23
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml40
-rw-r--r--app/views/projects/boards/components/sidebar/_due_date.html.haml32
-rw-r--r--app/views/projects/boards/components/sidebar/_labels.html.haml30
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml28
-rw-r--r--app/views/projects/boards/components/sidebar/_notifications.html.haml11
-rw-r--r--app/views/projects/boards/index.html.haml10
-rw-r--r--app/views/projects/boards/show.html.haml10
-rw-r--r--app/views/shared/icons/_icon_close.svg1
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb4
-rw-r--r--spec/features/boards/new_issue_spec.rb15
-rw-r--r--spec/features/boards/sidebar_spec.rb312
-rw-r--r--spec/fixtures/api/schemas/issue.json4
35 files changed, 900 insertions, 43 deletions
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index d4f8f4b9420..56f91503017 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -5,7 +5,9 @@
//= require_tree ./stores
//= require_tree ./services
//= require_tree ./mixins
+//= require_tree ./filters
//= require ./components/board
+//= require ./components/board_sidebar
//= require ./components/new_list_dropdown
//= require ./vue_resource_interceptor
@@ -22,7 +24,8 @@ $(() => {
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
- 'board': gl.issueBoards.Board
+ 'board': gl.issueBoards.Board,
+ 'board-sidebar': gl.issueBoards.BoardSidebar
},
data: {
state: Store.state,
@@ -30,9 +33,15 @@ $(() => {
endpoint: $boardApp.dataset.endpoint,
boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
- issueLinkBase: $boardApp.dataset.issueLinkBase
+ issueLinkBase: $boardApp.dataset.issueLinkBase,
+ detailIssue: Store.detail
},
init: Store.create.bind(Store),
+ computed: {
+ detailIssueVisible () {
+ return Object.keys(this.detailIssue.issue).length;
+ }
+ },
created () {
gl.boardService = new BoardService(this.endpoint, this.boardId);
},
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index cacb36a897f..500213a3a43 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -21,6 +21,7 @@
},
data () {
return {
+ detailIssue: Store.detail,
filters: Store.state.filters,
showIssueForm: false
};
@@ -32,6 +33,26 @@
this.list.getIssues(true);
},
deep: true
+ },
+ detailIssue: {
+ handler () {
+ if (!Object.keys(this.detailIssue.issue).length) return;
+
+ const issue = this.list.findIssue(this.detailIssue.issue.id);
+
+ if (issue) {
+ const boardsList = document.querySelectorAll('.boards-list')[0];
+ const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth;
+ const left = boardsList.scrollLeft - this.$el.offsetLeft;
+
+ if (right - boardsList.scrollLeft > 0) {
+ boardsList.scrollLeft = right;
+ } else if (left > 0) {
+ boardsList.scrollLeft = this.$el.offsetLeft;
+ }
+ }
+ },
+ deep: true
}
},
methods: {
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 4a7cfeaeab2..f743d72f936 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -12,6 +12,17 @@
disabled: Boolean,
index: Number
},
+ data () {
+ return {
+ showDetail: false,
+ detailIssue: Store.detail
+ };
+ },
+ computed: {
+ issueDetailVisible () {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ }
+ },
methods: {
filterByLabel (label, e) {
let labelToggleText = label.title;
@@ -37,6 +48,29 @@
$('.labels-filter .dropdown-toggle-text').text(labelToggleText);
Store.updateFiltersUrl();
+ },
+ mouseDown () {
+ this.showDetail = true;
+ },
+ mouseMove () {
+ if (this.showDetail) {
+ this.showDetail = false;
+ }
+ },
+ showIssue (e) {
+ const targetTagName = e.target.tagName.toLowerCase();
+
+ if (targetTagName === 'a' || targetTagName === 'button') return;
+
+ if (this.showDetail) {
+ this.showDetail = false;
+
+ if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
+ Store.detail.issue = {};
+ } else {
+ Store.detail.issue = this.issue;
+ }
+ }
}
}
});
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
index a4fad422eca..b31adfdb8da 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -1,4 +1,6 @@
(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
window.gl = window.gl || {};
gl.issueBoards.BoardNewIssue = Vue.extend({
@@ -27,13 +29,16 @@
const labels = this.list.label ? [this.list.label] : [];
const issue = new ListIssue({
title: this.title,
- labels
+ labels,
+ subscribed: true
});
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
+
+ Store.detail.issue = issue;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
new file mode 100644
index 00000000000..b20890df622
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -0,0 +1,51 @@
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardSidebar = Vue.extend({
+ props: {
+ currentUser: Object
+ },
+ data() {
+ return {
+ detail: Store.detail,
+ issue: {}
+ };
+ },
+ computed: {
+ showSidebar () {
+ return Object.keys(this.issue).length;
+ }
+ },
+ watch: {
+ detail: {
+ handler () {
+ this.issue = this.detail.issue;
+ },
+ deep: true
+ },
+ issue () {
+ if (this.showSidebar) {
+ this.$nextTick(() => {
+ $(".right-sidebar").getNiceScroll(0).doScrollTop(0, 0);
+ });
+ }
+ }
+ },
+ methods: {
+ closeSidebar () {
+ this.detail.issue = {};
+ }
+ },
+ ready () {
+ new IssuableContext(this.currentUser);
+ new MilestoneSelect();
+ new gl.DueDateSelectors();
+ new LabelsSelect();
+ new Sidebar();
+ new Subscription('.subscription');
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
new file mode 100644
index 00000000000..50ef1911022
--- /dev/null
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
@@ -0,0 +1,4 @@
+Vue.filter('due-date', (value) => {
+ const date = new Date(value);
+ return $.datepicker.formatDate('M d, yy', date);
+});
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index f629d45c587..bd9ba7d5118 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -22,7 +22,7 @@
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.has-tooltip, .btn',
- delay: gl.issueBoards.touchEnabled ? 100 : 0,
+ delay: gl.issueBoards.touchEnabled ? 100 : 50,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index eb082103de9..03fd6c05d87 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -3,12 +3,18 @@ class ListIssue {
this.id = obj.iid;
this.title = obj.title;
this.confidential = obj.confidential;
+ this.dueDate = obj.due_date;
+ this.subscribed = obj.subscribed;
this.labels = [];
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee);
}
+ if (obj.milestone) {
+ this.milestone = new ListMilestone(obj.milestone);
+ }
+
obj.labels.forEach((label) => {
this.labels.push(new ListLabel(label));
});
@@ -41,4 +47,21 @@ class ListIssue {
getLists () {
return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
}
+
+ update (url) {
+ const data = {
+ issue: {
+ milestone_id: this.milestone ? this.milestone.id : null,
+ due_date: this.dueDate,
+ assignee_id: this.assignee ? this.assignee.id : null,
+ label_ids: this.labels.map( (label) => label.id )
+ }
+ };
+
+ if (!data.issue.label_ids.length) {
+ data.issue.label_ids = [''];
+ }
+
+ return Vue.http.patch(url, data);
+ }
}
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6
new file mode 100644
index 00000000000..577adf11265
--- /dev/null
+++ b/app/assets/javascripts/boards/models/milestone.js.es6
@@ -0,0 +1,6 @@
+class ListMilestone {
+ constructor (obj) {
+ this.id = obj.id;
+ this.title = obj.title;
+ }
+}
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index b9c91cbf31e..b76063911bb 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,7 +1,5 @@
class BoardService {
constructor (root, boardId) {
- Vue.http.options.root = root;
-
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index bd07ee0c161..a6c5739ee41 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -5,6 +5,9 @@
gl.issueBoards.BoardsStore = {
disabled: false,
state: {},
+ detail: {
+ issue: {}
+ },
moving: {
issue: {},
list: {}
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index 41925fcc8e3..4f7c1092d05 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -41,7 +41,12 @@
defaultDate: $("input[name='" + this.fieldName + "']").val(),
altField: "input[name='" + this.fieldName + "']",
onSelect: () => {
- return this.saveDueDate(true);
+ if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = $("input[name='" + this.fieldName + "']").val();
+ this.updateIssueBoardIssue();
+ } else {
+ return this.saveDueDate(true);
+ }
}
});
}
@@ -49,8 +54,14 @@
initRemoveDueDate() {
this.$block.on('click', '.js-remove-due-date', (e) => {
e.preventDefault();
- $("input[name='" + this.fieldName + "']").val('');
- return this.saveDueDate(false);
+
+ if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
+ this.updateIssueBoardIssue();
+ } else {
+ $("input[name='" + this.fieldName + "']").val('');
+ return this.saveDueDate(false);
+ }
});
}
@@ -83,6 +94,18 @@
this.datePayload = datePayload;
}
+ updateIssueBoardIssue () {
+ this.$loading.fadeIn();
+ this.$dropdown.trigger('loading.gl.dropdown');
+ this.$selectbox.hide();
+ this.$value.css('display', '');
+
+ gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
+ .then(() => {
+ this.$loading.fadeOut();
+ });
+ }
+
submitSelectedDate(isDropdown) {
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 53762f2965c..7620ae41729 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -620,6 +620,17 @@
selectedObject = this.renderedData[selectedIndex];
}
}
+
+ if (this.options.vue) {
+ if (el.hasClass(ACTIVE_CLASS)) {
+ el.removeClass(ACTIVE_CLASS);
+ } else {
+ el.addClass(ACTIVE_CLASS);
+ }
+
+ return selectedObject;
+ }
+
field = [];
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index b4f6e70f694..4e29ab71bd4 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -22,7 +22,7 @@
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
$block = $selectbox.closest('.block');
- $form = $dropdown.closest('form');
+ $form = $dropdown.closest('form, .js-issuable-update');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
@@ -317,6 +317,7 @@
}
},
multiSelect: $dropdown.hasClass('js-multiselect'),
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e) {
var isIssueIndex, isMRIndex, page;
_this.enableBulkLabelDropdown();
@@ -334,7 +335,7 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
- if ($('html').hasClass('issue-boards-page')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
}
@@ -362,6 +363,30 @@
else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
}
+ else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($el.hasClass('is-active')) {
+ gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
+ id: label.id,
+ title: label.title,
+ color: label.color[0],
+ textColor: '#fff'
+ }));
+ }
+ else {
+ var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
+ labels = labels.filter(function (selectedLabel) {
+ return selectedLabel.id !== label.id;
+ });
+ gl.issueBoards.BoardsStore.detail.issue.labels = labels;
+ }
+
+ $loading.fadeIn();
+
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(function () {
+ $loading.fadeOut();
+ });
+ }
else {
if ($dropdown.hasClass('js-multiselect')) {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index cee42633c79..95b5b934c81 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -101,6 +101,7 @@
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(selected, $el, e) {
var data, isIssueIndex, isMRIndex, page;
page = $('body').data('page');
@@ -110,7 +111,7 @@
e.preventDefault();
return;
}
- if ($('html').hasClass('issue-boards-page')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault();
@@ -123,6 +124,24 @@
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if (selected.id !== -1) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({
+ id: selected.id,
+ title: selected.name
+ }));
+ } else {
+ Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone');
+ }
+
+ $dropdown.trigger('loading.gl.dropdown');
+ $loading.fadeIn();
+
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(function () {
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.fadeOut();
+ });
} else {
selected = $selectbox.find('input[type="hidden"]').val();
data = {};
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index e3d5f413c77..221460f32e7 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -5,15 +5,24 @@
function Sidebar(currentUser) {
this.toggleTodo = bind(this.toggleTodo, this);
this.sidebar = $('aside');
+ this.removeListeners();
this.addEventListeners();
}
+ Sidebar.prototype.removeListeners = function () {
+ this.sidebar.off('click', '.sidebar-collapsed-icon');
+ $('.dropdown').off('hidden.gl.dropdown');
+ $('.dropdown').off('loading.gl.dropdown');
+ $('.dropdown').off('loaded.gl.dropdown');
+ $(document).off('click', '.js-sidebar-toggle');
+ }
+
Sidebar.prototype.addEventListeners = function() {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
- $(document).off('click', '.js-sidebar-toggle').on('click', '.js-sidebar-toggle', function(e, triggered) {
+ $(document).on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
$this = $(this);
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
index 5e3c5983d75..bfa3663bca3 100644
--- a/app/assets/javascripts/subscription.js
+++ b/app/assets/javascripts/subscription.js
@@ -5,10 +5,10 @@
function Subscription(container) {
this.toggleSubscription = bind(this.toggleSubscription, this);
var $container;
- $container = $(container);
- this.url = $container.attr('data-url');
- this.subscribe_button = $container.find('.js-subscribe-button');
- this.subscription_status = $container.find('.subscription-status');
+ this.$container = $(container);
+ this.url = this.$container.attr('data-url');
+ this.subscribe_button = this.$container.find('.js-subscribe-button');
+ this.subscription_status = this.$container.find('.subscription-status');
this.subscribe_button.unbind('click').click(this.toggleSubscription);
}
@@ -18,17 +18,27 @@
action = btn.find('span').text();
current_status = this.subscription_status.attr('data-status');
btn.addClass('disabled');
+
+ if ($('html').hasClass('issue-boards-page')) {
+ this.url = this.$container.attr('data-url');
+ }
+
return $.post(this.url, (function(_this) {
return function() {
var status;
btn.removeClass('disabled');
- status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
- _this.subscription_status.attr('data-status', status);
- action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
- btn.find('span').text(action);
- _this.subscription_status.find('>div').toggleClass('hidden');
- if (btn.attr('data-original-title')) {
- return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
+
+ if ($('html').hasClass('issue-boards-page')) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed);
+ } else {
+ status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
+ _this.subscription_status.attr('data-status', status);
+ action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
+ btn.find('span').text(action);
+ _this.subscription_status.find('>div').toggleClass('hidden');
+ if (btn.attr('data-original-title')) {
+ return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
+ }
}
};
})(this));
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 3020b7cc239..da6a59bcf97 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -9,7 +9,11 @@
this.usersPath = "/autocomplete/users.json";
this.userPath = "/autocomplete/users/:id.json";
if (currentUser != null) {
- this.currentUser = JSON.parse(currentUser);
+ if (typeof currentUser === 'object') {
+ this.currentUser = currentUser;
+ } else {
+ this.currentUser = JSON.parse(currentUser);
+ }
}
$('.js-user-search').each((function(_this) {
return function(i, dropdown) {
@@ -32,9 +36,30 @@
$value = $block.find('.value');
$collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut();
+
+ var updateIssueBoardsIssue = function () {
+ $loading.fadeIn();
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(function () {
+ $loading.fadeOut();
+ });
+ };
+
$block.on('click', '.js-assign-yourself', function(e) {
e.preventDefault();
- return assignTo(_this.currentUser.id);
+
+ if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
+ id: _this.currentUser.id,
+ username: _this.currentUser.username,
+ name: _this.currentUser.name,
+ avatar_url: _this.currentUser.avatar_url
+ }));
+
+ updateIssueBoardsIssue();
+ } else {
+ return assignTo(_this.currentUser.id);
+ }
});
assignTo = function(selected) {
var data;
@@ -150,6 +175,7 @@
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected;
page = $('body').data('page');
@@ -160,7 +186,7 @@
selectedId = user.id;
return;
}
- if ($('html').hasClass('issue-boards-page')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl();
@@ -170,6 +196,19 @@
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if (user.id) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
+ id: user.id,
+ username: user.username,
+ name: user.name,
+ avatar_url: user.avatar_url
+ }));
+ } else {
+ Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee');
+ }
+
+ updateIssueBoardsIssue();
} else {
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
return assignTo(selected);
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index d8fabbdcebe..3dda2042cb3 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -45,6 +45,15 @@
.page-with-sidebar {
padding-bottom: 0;
}
+
+ .issues-filters {
+ position: relative;
+ z-index: 999999;
+ }
+}
+
+.boards-app {
+ position: relative;
}
.boards-app-loading {
@@ -66,6 +75,10 @@
height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px);
min-height: 475px;
+
+ &.is-compact {
+ width: calc(100% - 290px);
+ }
}
}
@@ -184,6 +197,10 @@
margin-bottom: 5px;
}
+ &.is-active {
+ background-color: $row-hover;
+ }
+
.label {
border: 0;
outline: 0;
@@ -264,3 +281,45 @@
border-width: 1px 0 1px 1px;
}
}
+
+.issue-boards-sidebar {
+ &.right-sidebar {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ }
+
+ .issuable-sidebar-header {
+ position: relative;
+ }
+
+ .gutter-toggle {
+ position: absolute;
+ top: 0;
+ bottom: 10px;
+ right: 0;
+ width: 22px;
+ color: $gray-darkest;
+
+ svg {
+ position: absolute;
+ top: 50%;
+ margin-top: (-11px / 2);
+ }
+
+ &:hover {
+ path {
+ fill: $gray-darkest;
+ }
+ }
+ }
+
+ .issuable-header-text {
+ width: 100%;
+ padding-right: 35px;
+
+ > strong {
+ font-weight: 600;
+ }
+ }
+}
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index a2b01ff43dc..dc33e1405f2 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -73,10 +73,13 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
labels: true,
- only: [:iid, :title, :confidential],
+ only: [:iid, :title, :confidential, :due_date],
include: {
- assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
- })
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+ milestone: { only: [:id, :title] }
+ },
+ user: current_user
+ )
end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 133a5993815..ef92ac27b46 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -281,10 +281,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
+ json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
+
if options.has_key?(:labels)
json[:labels] = labels.as_json(
project: project,
- only: [:id, :title, :description, :color],
+ only: [:id, :title, :description, :color, :priority],
methods: [:text_color]
)
end
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index d8f16022407..5cfd42485c5 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -7,8 +7,11 @@
":issue-link-base" => "issueLinkBase",
":disabled" => "disabled",
"track-by" => "id" }
- %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }",
- ":index" => "index" }
+ %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
+ ":index" => "index",
+ "@mousedown" => "mouseDown",
+ "@mouseMove" => "mouseMove",
+ "@mouseup" => "showIssue($event)" }
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => "issueLinkBase + '/' + issue.id",
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
new file mode 100644
index 00000000000..f0c0c6953e0
--- /dev/null
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -0,0 +1,23 @@
+%board-sidebar{ "inline-template" => true,
+ ":current-user" => "#{current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) if current_user}" }
+ %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
+ .issuable-sidebar
+ .block.issuable-sidebar-header
+ %span.issuable-header-text.hide-collapsed.pull-left
+ %strong
+ {{ issue.title }}
+ %br/
+ %span
+ = precede "#" do
+ {{ issue.id }}
+ %a.gutter-toggle.pull-right{ role: "button",
+ href: "#",
+ "@click.prevent" => "closeSidebar",
+ "aria-label" => "Toggle sidebar" }
+ = custom_icon("icon_close", size: 15)
+ .js-issuable-update
+ = render "projects/boards/components/sidebar/assignee"
+ = render "projects/boards/components/sidebar/milestone"
+ = render "projects/boards/components/sidebar/due_date"
+ = render "projects/boards/components/sidebar/labels"
+ = render "projects/boards/components/sidebar/notifications"
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
new file mode 100644
index 00000000000..b90db421af0
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -0,0 +1,40 @@
+.block.assignee
+ .title.hide-collapsed
+ Assignee
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value.hide-collapsed
+ %span.assign-yourself.no-value{ "v-if" => "!issue.assignee" }
+ No assignee
+ - if can?(current_user, :admin_issue, @project)
+ \-
+ %a.js-assign-yourself{ href: "#" }
+ assign yourself
+ %a.author_link.bold{ href: "",
+ "v-if" => "issue.assignee" }
+ %img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar",
+ width: "32" }
+ %span.author
+ {{ issue.assignee.name }}
+ %span.username
+ = precede "@" do
+ {{ issue.assignee.username }}
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox.hide-collapsed
+ %input{ type: "hidden",
+ name: "issue[assignee_id]",
+ id: "issue_assignee_id",
+ ":value" => "issue.assignee.id",
+ "v-if" => "issue.assignee" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" },
+ ":data-issuable-id" => "issue.id",
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ Select assignee
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
+ = dropdown_title("Assign to")
+ = dropdown_filter("Search users")
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
new file mode 100644
index 00000000000..c7da1d0d4ac
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml
@@ -0,0 +1,32 @@
+.block.due_date
+ .title
+ Due date
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value
+ .value-content
+ %span.no-value{ "v-if" => "!issue.dueDate" }
+ No due date
+ %span.bold{ "v-if" => "issue.dueDate" }
+ {{ issue.dueDate | due-date }}
+ - if can?(current_user, :admin_issue, @project)
+ %span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
+ \-
+ %a.js-remove-due-date{ href: "#", role: "button" }
+ remove due date
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox
+ %input{ type: "hidden",
+ name: "issue[due_date]",
+ ":value" => "issue.dueDate" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
+ data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" },
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ %span.dropdown-toggle-text Due date
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-menu-due-date
+ = dropdown_title('Due date')
+ = dropdown_content do
+ .js-due-date-calendar
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
new file mode 100644
index 00000000000..a6d2c3c300c
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_labels.html.haml
@@ -0,0 +1,30 @@
+.block.labels
+ .title
+ Labels
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value.issuable-show-labels
+ %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
+ None
+ %a{ href: "#",
+ "v-for" => "label in issue.labels" }
+ %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
+ {{ label.title }}
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox
+ %input{ type: "hidden",
+ name: "issue[label_names][]",
+ "v-for" => "label in issue.labels",
+ ":value" => "label.id" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
+ data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project.id, labels: namespace_project_labels_path(@project.namespace, @project, :json) },
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ %span.dropdown-toggle-text
+ Label
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ = render partial: "shared/issuable/label_page_default"
+ - if can? current_user, :admin_label, @project and @project
+ = render partial: "shared/issuable/label_page_create"
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
new file mode 100644
index 00000000000..3cd20d1c0f7
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -0,0 +1,28 @@
+.block.milestone
+ .title
+ Milestone
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value
+ %span.no-value{ "v-if" => "!issue.milestone" }
+ None
+ %span.bold.has-tooltip{ "v-if" => "issue.milestone" }
+ {{ issue.milestone.title }}
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox
+ %input{ type: "hidden",
+ ":value" => "issue.milestone.id",
+ name: "issue[milestone_id]",
+ "v-if" => "issue.milestone" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: "issue", use_id: "true" },
+ ":data-issuable-id" => "issue.id",
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ Milestone
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ = dropdown_title("Assignee milestone")
+ = dropdown_filter("Search milestones")
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml
new file mode 100644
index 00000000000..21c9563e9db
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml
@@ -0,0 +1,11 @@
+- if current_user
+ .block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" }
+ .title
+ Notifications
+ %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }}
+ .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" }
+ .unsubscribed{ "v-show" => "!issue.subscribed" }
+ You're not receiving notifications from this thread.
+ .subscribed{ "v-show" => "issue.subscribed" }
+ You're receiving notifications because you're subscribed to this thread.
diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml
index 885f8e34b55..29c9a43a0c1 100644
--- a/app/views/projects/boards/index.html.haml
+++ b/app/views/projects/boards/index.html.haml
@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards
-.boards-list#board-app{ "v-cloak" => true, data: board_data }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+ .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ = render "projects/boards/components/board"
+ = render "projects/boards/components/sidebar"
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
index 885f8e34b55..29c9a43a0c1 100644
--- a/app/views/projects/boards/show.html.haml
+++ b/app/views/projects/boards/show.html.haml
@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards
-.boards-list#board-app{ "v-cloak" => true, data: board_data }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+ .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ = render "projects/boards/components/board"
+ = render "projects/boards/components/sidebar"
diff --git a/app/views/shared/icons/_icon_close.svg b/app/views/shared/icons/_icon_close.svg
new file mode 100644
index 00000000000..9d62012518b
--- /dev/null
+++ b/app/views/shared/icons/_icon_close.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg> \ No newline at end of file
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index da59642f24d..cbe0417a4a7 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -21,9 +21,11 @@ describe Projects::Boards::IssuesController do
context 'with valid list id' do
it 'returns issues that have the list label applied' do
johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+ issue = create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [planning])
- create(:labeled_issue, project: project, labels: [development])
+ create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+ issue.subscribe(johndoe)
list_issues user: user, board: board, list: list2
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 67d6da5f39a..760a8967123 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -66,6 +66,21 @@ describe 'Issue Boards new issue', feature: true, js: true do
expect(page).to have_content('1')
end
end
+
+ it 'shows sidebar when creating new issue' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+ end
+
+ page.within(first('.board-new-issue-form')) do
+ find('.form-control').set('bug')
+ click_button 'Submit issue'
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+ end
end
context 'unauthorized user' do
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
new file mode 100644
index 00000000000..f160052a844
--- /dev/null
+++ b/spec/features/boards/sidebar_spec.rb
@@ -0,0 +1,312 @@
+require 'rails_helper'
+
+describe 'Issue Boards', feature: true, js: true do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let!(:label) { create(:label, project: project) }
+ let!(:label2) { create(:label, project: project) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) }
+ let!(:issue) { 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
+
+ it 'shows sidebar when clicking issue' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+ end
+
+ it 'closes sidebar when clicking issue' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).not_to have_selector('.issue-boards-sidebar')
+ end
+
+ it 'closes sidebar when clicking close button' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+
+ find('.gutter-toggle').click
+
+ expect(page).not_to have_selector('.issue-boards-sidebar')
+ end
+
+ it 'shows issue details when sidebar is open' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.issue-boards-sidebar') do
+ expect(page).to have_content(issue.title)
+ expect(page).to have_content(issue.to_reference)
+ end
+ end
+
+ context 'assignee' do
+ it 'updates the issues assignee' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.assignee') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ page.within('.dropdown-menu-user') do
+ click_link user.name
+
+ wait_for_vue_resource
+ end
+
+ expect(page).to have_content(user.name)
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.avatar')
+ end
+ end
+ end
+
+ it 'removes the assignee' do
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.assignee') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ page.within('.dropdown-menu-user') do
+ click_link 'Unassigned'
+
+ wait_for_vue_resource
+ end
+
+ expect(page).to have_content('No assignee')
+ end
+
+ page.within(first('.board')) do
+ page.within(find('.card:nth-child(2)')) do
+ expect(page).not_to have_selector('.avatar')
+ end
+ end
+ end
+
+ it 'assignees to current user' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.assignee') do
+ click_link 'assign yourself'
+
+ wait_for_vue_resource
+
+ expect(page).to have_content(user.name)
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.avatar')
+ end
+ end
+ end
+ end
+
+ context 'milestone' do
+ it 'adds a milestone' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.milestone') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link milestone.title
+
+ wait_for_vue_resource
+
+ page.within('.value') do
+ expect(page).to have_content(milestone.title)
+ end
+ end
+ end
+
+ it 'removes a milestone' do
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.milestone') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link "No Milestone"
+
+ wait_for_vue_resource
+
+ page.within('.value') do
+ expect(page).not_to have_content(milestone.title)
+ end
+ end
+ end
+ end
+
+ context 'due date' do
+ it 'updates due date' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.due_date') do
+ click_link 'Edit'
+
+ click_link Date.today.day
+
+ wait_for_vue_resource
+
+ expect(page).to have_content(Date.today.to_s(:medium))
+ end
+ end
+ end
+
+ context 'labels' do
+ it 'adds a single label' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link label.title
+
+ wait_for_vue_resource
+
+ find('.dropdown-menu-close-icon').click
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 1)
+ expect(page).to have_content(label.title)
+ end
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.label', count: 1)
+ expect(page).to have_content(label.title)
+ end
+ end
+ end
+
+ it 'adds a multiple labels' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link label.title
+ click_link label2.title
+
+ wait_for_vue_resource
+
+ find('.dropdown-menu-close-icon').click
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_content(label.title)
+ expect(page).to have_content(label2.title)
+ end
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_content(label.title)
+ expect(page).to have_content(label2.title)
+ end
+ end
+ end
+
+ it 'removes a label' do
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link label.title
+
+ wait_for_vue_resource
+
+ find('.dropdown-menu-close-icon').click
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 0)
+ expect(page).not_to have_content(label.title)
+ end
+ end
+
+ page.within(first('.board')) do
+ page.within(find('.card:nth-child(2)')) do
+ expect(page).not_to have_selector('.label', count: 1)
+ expect(page).not_to have_content(label.title)
+ end
+ end
+ end
+ end
+
+ context 'subscription' do
+ it 'changes issue subscription' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.subscription') do
+ click_button 'Subscribe'
+
+ expect(page).to have_content("You're receiving notifications because you're subscribed to this thread.")
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 532ebb9640e..77f2bcee1f3 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -9,6 +9,7 @@
"iid": { "type": "integer" },
"title": { "type": "string" },
"confidential": { "type": "boolean" },
+ "due_date": { "type": ["date", "null"] },
"labels": {
"type": "array",
"items": {
@@ -42,7 +43,8 @@
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
- }
+ },
+ "subscribed": { "type": ["boolean", "null"] }
},
"additionalProperties": false
}