Commit a66a3c74 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'issue-board-sidebar' into 'master'

Issue board sidebar

## What does this MR do?

Adds a sidebar when clicking an issue in the issue boards lists. This allows user to easily update other parts of the issue details without having to visit the issue itself. Same functionality as on issue page.

When creating a new issue the sidebar automatically opens.

## Screenshots (if relevant)

![Screen_Shot_2016-10-07_at_13.10.16](/uploads/ad08785f407d8ac3fe9cb078868a7839/Screen_Shot_2016-10-07_at_13.10.16.png)

## What are the relevant issue numbers?

Closes #21219

See merge request !6690
parents b0e6fc12 a2eff1a8
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
//= require_tree ./stores //= require_tree ./stores
//= require_tree ./services //= require_tree ./services
//= require_tree ./mixins //= require_tree ./mixins
//= require_tree ./filters
//= require ./components/board //= require ./components/board
//= require ./components/board_sidebar
//= require ./components/new_list_dropdown //= require ./components/new_list_dropdown
//= require ./vue_resource_interceptor //= require ./vue_resource_interceptor
...@@ -22,7 +24,8 @@ $(() => { ...@@ -22,7 +24,8 @@ $(() => {
gl.IssueBoardsApp = new Vue({ gl.IssueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
components: { components: {
'board': gl.issueBoards.Board 'board': gl.issueBoards.Board,
'board-sidebar': gl.issueBoards.BoardSidebar
}, },
data: { data: {
state: Store.state, state: Store.state,
...@@ -30,9 +33,15 @@ $(() => { ...@@ -30,9 +33,15 @@ $(() => {
endpoint: $boardApp.dataset.endpoint, endpoint: $boardApp.dataset.endpoint,
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,
detailIssue: Store.detail
}, },
init: Store.create.bind(Store), init: Store.create.bind(Store),
computed: {
detailIssueVisible () {
return Object.keys(this.detailIssue.issue).length;
}
},
created () { created () {
gl.boardService = new BoardService(this.endpoint, this.boardId); gl.boardService = new BoardService(this.endpoint, this.boardId);
}, },
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
}, },
data () { data () {
return { return {
detailIssue: Store.detail,
filters: Store.state.filters, filters: Store.state.filters,
showIssueForm: false showIssueForm: false
}; };
...@@ -32,6 +33,26 @@ ...@@ -32,6 +33,26 @@
this.list.getIssues(true); this.list.getIssues(true);
}, },
deep: 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: { methods: {
......
...@@ -12,6 +12,17 @@ ...@@ -12,6 +12,17 @@
disabled: Boolean, disabled: Boolean,
index: Number index: Number
}, },
data () {
return {
showDetail: false,
detailIssue: Store.detail
};
},
computed: {
issueDetailVisible () {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
}
},
methods: { methods: {
filterByLabel (label, e) { filterByLabel (label, e) {
let labelToggleText = label.title; let labelToggleText = label.title;
...@@ -37,6 +48,29 @@ ...@@ -37,6 +48,29 @@
$('.labels-filter .dropdown-toggle-text').text(labelToggleText); $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
Store.updateFiltersUrl(); 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;
}
}
} }
} }
}); });
......
(() => { (() => {
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.issueBoards.BoardNewIssue = Vue.extend({ gl.issueBoards.BoardNewIssue = Vue.extend({
...@@ -27,13 +29,16 @@ ...@@ -27,13 +29,16 @@
const labels = this.list.label ? [this.list.label] : []; const labels = this.list.label ? [this.list.label] : [];
const issue = new ListIssue({ const issue = new ListIssue({
title: this.title, title: this.title,
labels labels,
subscribed: true
}); });
this.list.newIssue(issue) this.list.newIssue(issue)
.then((data) => { .then((data) => {
// 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
$(this.$els.submitButton).enable(); $(this.$els.submitButton).enable();
Store.detail.issue = issue;
}) })
.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
......
(() => {
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);
$('.right-sidebar').getNiceScroll().resize();
});
}
}
},
methods: {
closeSidebar () {
this.detail.issue = {};
}
},
ready () {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new gl.DueDateSelectors();
new LabelsSelect();
new Sidebar();
new Subscription('.subscription');
}
});
})();
Vue.filter('due-date', (value) => {
const date = new Date(value);
return $.datepicker.formatDate('M d, yy', date);
});
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
fallbackOnBody: true, fallbackOnBody: true,
ghostClass: 'is-ghost', ghostClass: 'is-ghost',
filter: '.has-tooltip, .btn', filter: '.has-tooltip, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0, delay: gl.issueBoards.touchEnabled ? 100 : 50,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20, scrollSpeed: 20,
onStart: gl.issueBoards.onStart, onStart: gl.issueBoards.onStart,
......
...@@ -3,12 +3,18 @@ class ListIssue { ...@@ -3,12 +3,18 @@ class ListIssue {
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.subscribed = obj.subscribed;
this.labels = []; this.labels = [];
if (obj.assignee) { if (obj.assignee) {
this.assignee = new ListUser(obj.assignee); this.assignee = new ListUser(obj.assignee);
} }
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
}
obj.labels.forEach((label) => { obj.labels.forEach((label) => {
this.labels.push(new ListLabel(label)); this.labels.push(new ListLabel(label));
}); });
...@@ -41,4 +47,21 @@ class ListIssue { ...@@ -41,4 +47,21 @@ class ListIssue {
getLists () { getLists () {
return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) ); 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);
}
} }
class ListMilestone {
constructor (obj) {
this.id = obj.id;
this.title = obj.title;
}
}
class BoardService { class BoardService {
constructor (root, boardId) { constructor (root, boardId) {
Vue.http.options.root = root;
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: { generate: {
method: 'POST', method: 'POST',
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
gl.issueBoards.BoardsStore = { gl.issueBoards.BoardsStore = {
disabled: false, disabled: false,
state: {}, state: {},
detail: {
issue: {}
},
moving: { moving: {
issue: {}, issue: {},
list: {} list: {}
......
...@@ -41,16 +41,27 @@ ...@@ -41,16 +41,27 @@
defaultDate: $("input[name='" + this.fieldName + "']").val(), defaultDate: $("input[name='" + this.fieldName + "']").val(),
altField: "input[name='" + this.fieldName + "']", altField: "input[name='" + this.fieldName + "']",
onSelect: () => { onSelect: () => {
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); return this.saveDueDate(true);
} }
}
}); });
} }
initRemoveDueDate() { initRemoveDueDate() {
this.$block.on('click', '.js-remove-due-date', (e) => { this.$block.on('click', '.js-remove-due-date', (e) => {
e.preventDefault(); e.preventDefault();
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
this.updateIssueBoardIssue();
} else {
$("input[name='" + this.fieldName + "']").val(''); $("input[name='" + this.fieldName + "']").val('');
return this.saveDueDate(false); return this.saveDueDate(false);
}
}); });
} }
...@@ -83,6 +94,18 @@ ...@@ -83,6 +94,18 @@
this.datePayload = datePayload; 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) { submitSelectedDate(isDropdown) {
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
......
...@@ -622,6 +622,17 @@ ...@@ -622,6 +622,17 @@
selectedObject = this.renderedData[selectedIndex]; 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 = []; field = [];
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) { if (isInput) {
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
abilityName = $dropdown.data('ability-name'); abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox'); $selectbox = $dropdown.closest('.selectbox');
$block = $selectbox.closest('.block'); $block = $selectbox.closest('.block');
$form = $dropdown.closest('form'); $form = $dropdown.closest('form, .js-issuable-update');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span'); $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value'); $value = $block.find('.value');
...@@ -317,6 +317,7 @@ ...@@ -317,6 +317,7 @@
} }
}, },
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e) { clicked: function(label, $el, e) {
var isIssueIndex, isMRIndex, page; var isIssueIndex, isMRIndex, page;
_this.enableBulkLabelDropdown(); _this.enableBulkLabelDropdown();
...@@ -334,7 +335,7 @@ ...@@ -334,7 +335,7 @@
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests: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) { if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = []; gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
} }
...@@ -362,6 +363,30 @@ ...@@ -362,6 +363,30 @@
else if ($dropdown.hasClass('js-filter-submit')) { else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').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 { else {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
......
...@@ -101,6 +101,7 @@ ...@@ -101,6 +101,7 @@
// display:block overrides the hide-collapse rule // display:block overrides the hide-collapse rule
return $value.css('display', ''); return $value.css('display', '');
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(selected, $el, e) { clicked: function(selected, $el, e) {
var data, isIssueIndex, isMRIndex, page; var data, isIssueIndex, isMRIndex, page;
page = $('body').data('page'); page = $('body').data('page');
...@@ -110,7 +111,7 @@ ...@@ -110,7 +111,7 @@
e.preventDefault(); e.preventDefault();
return; 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.state.filters[$dropdown.data('field-name')] = selected.name;
gl.issueBoards.BoardsStore.updateFiltersUrl(); gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault(); e.preventDefault();
...@@ -123,6 +124,24 @@ ...@@ -123,6 +124,24 @@
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').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 { } else {
selected = $selectbox.find('input[type="hidden"]').val(); selected = $selectbox.find('input[type="hidden"]').val();
data = {}; data = {};
......
...@@ -5,15 +5,24 @@ ...@@ -5,15 +5,24 @@
function Sidebar(currentUser) { function Sidebar(currentUser) {
this.toggleTodo = bind(this.toggleTodo, this); this.toggleTodo = bind(this.toggleTodo, this);
this.sidebar = $('aside'); this.sidebar = $('aside');
this.removeListeners();
this.addEventListeners(); 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() { Sidebar.prototype.addEventListeners = function() {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.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; var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault(); e.preventDefault();
$this = $(this); $this = $(this);
......
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
function Subscription(container) { function Subscription(container) {
this.toggleSubscription = bind(this.toggleSubscription, this); this.toggleSubscription = bind(this.toggleSubscription, this);
var $container; var $container;
$container = $(container); this.$container = $(container);
this.url = $container.attr('data-url'); this.url = this.$container.attr('data-url');
this.subscribe_button = $container.find('.js-subscribe-button'); this.subscribe_button = this.$container.find('.js-subscribe-button');
this.subscription_status = $container.find('.subscription-status'); this.subscription_status = this.$container.find('.subscription-status');
this.subscribe_button.unbind('click').click(this.toggleSubscription); this.subscribe_button.unbind('click').click(this.toggleSubscription);
} }
...@@ -18,10 +18,19 @@ ...@@ -18,10 +18,19 @@
action = btn.find('span').text(); action = btn.find('span').text();
current_status = this.subscription_status.attr('data-status'); current_status = this.subscription_status.attr('data-status');
btn.addClass('disabled'); btn.addClass('disabled');
if ($('html').hasClass('issue-boards-page')) {
this.url = this.$container.attr('data-url');
}
return $.post(this.url, (function(_this) { return $.post(this.url, (function(_this) {
return function() { return function() {
var status; var status;
btn.removeClass('disabled'); btn.removeClass('disabled');
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'; status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
_this.subscription_status.attr('data-status', status); _this.subscription_status.attr('data-status', status);
action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
...@@ -30,6 +39,7 @@ ...@@ -30,6 +39,7 @@
if (btn.attr('data-original-title')) { if (btn.attr('data-original-title')) {
return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
} }
}
}; };
})(this)); })(this));
}; };
......
...@@ -9,8 +9,12 @@ ...@@ -9,8 +9,12 @@
this.usersPath = "/autocomplete/users.json"; this.usersPath = "/autocomplete/users.json";
this.userPath = "/autocomplete/users/:id.json"; this.userPath = "/autocomplete/users/:id.json";
if (currentUser != null) { if (currentUser != null) {
if (typeof currentUser === 'object') {
this.currentUser = currentUser;
} else {
this.currentUser = JSON.parse(currentUser); this.currentUser = JSON.parse(currentUser);
} }
}
$('.js-user-search').each((function(_this) { $('.js-user-search').each((function(_this) {
return function(i, dropdown) { return function(i, dropdown) {
var options = {}; var options = {};
...@@ -32,9 +36,30 @@ ...@@ -32,9 +36,30 @@
$value = $block.find('.value'); $value = $block.find('.value');
$collapsedSidebar = $block.find('.sidebar-collapsed-user'); $collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut(); $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) { $block.on('click', '.js-assign-yourself', function(e) {
e.preventDefault(); e.preventDefault();
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); return assignTo(_this.currentUser.id);
}
}); });
assignTo = function(selected) { assignTo = function(selected) {
var data; var data;
...@@ -150,6 +175,7 @@ ...@@ -150,6 +175,7 @@
// display:block overrides the hide-collapse rule // display:block overrides the hide-collapse rule
return $value.css('display', ''); return $value.css('display', '');
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(user, $el, e) { clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected; var isIssueIndex, isMRIndex, page, selected;
page = $('body').data('page'); page = $('body').data('page');
...@@ -160,7 +186,7 @@ ...@@ -160,7 +186,7 @@
selectedId = user.id; selectedId = user.id;
return; return;
} }
if ($('html').hasClass('issue-boards-page')) { if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = user.id; selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl(); gl.issueBoards.BoardsStore.updateFiltersUrl();
...@@ -170,6 +196,19 @@ ...@@ -170,6 +196,19 @@
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').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 { } else {
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
return assignTo(selected); return assignTo(selected);
......
...@@ -45,6 +45,15 @@ ...@@ -45,6 +45,15 @@
.page-with-sidebar { .page-with-sidebar {
padding-bottom: 0; padding-bottom: 0;
} }
.issues-filters {
position: relative;
z-index: 999999;
}
}
.boards-app {
position: relative;
} }
.boards-app-loading { .boards-app-loading {
...@@ -66,6 +75,10 @@ ...@@ -66,6 +75,10 @@
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px); height: calc(100vh - 220px);
min-height: 475px; min-height: 475px;
&.is-compact {
width: calc(100% - 290px);
}
} }
} }
...@@ -184,6 +197,10 @@ ...@@ -184,6 +197,10 @@
margin-bottom: 5px; margin-bottom: 5px;
} }
&.is-active {
background-color: $row-hover;
}
.label { .label {
border: 0; border: 0;
outline: 0; outline: 0;
...@@ -212,6 +229,10 @@ ...@@ -212,6 +229,10 @@
margin-right: 5px; margin-right: 5px;
font-size: (14px / $issue-boards-font-size) * 1em; font-size: (14px / $issue-boards-font-size) * 1em;
} }
.avatar {
margin-left: 0;
}
} }
.card-number { .card-number {
...@@ -264,3 +285,48 @@ ...@@ -264,3 +285,48 @@
border-width: 1px 0 1px 1px; border-width: 1px 0 1px 1px;
} }
} }
.issue-boards-sidebar {
&.right-sidebar {
top: 153px;
bottom: 0;
@media (min-width: $screen-sm-min) {
top: 220px;
}
}
.issuable-sidebar-header {
position: relative;
}
.gutter-toggle {
position: absolute;
top: 0;
bottom: 15px;
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;
}
}
}
...@@ -73,10 +73,13 @@ module Projects ...@@ -73,10 +73,13 @@ 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], only: [: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] }
},
user: current_user
)
end end
end end
end end
......
...@@ -287,10 +287,12 @@ class Issue < ActiveRecord::Base ...@@ -287,10 +287,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {}) def as_json(options = {})
super(options).tap do |json| super(options).tap do |json|
json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
if options.has_key?(:labels) if options.has_key?(:labels)
json[:labels] = labels.as_json( json[:labels] = labels.as_json(
project: project, project: project,
only: [:id, :title, :description, :color], only: [:id, :title, :description, :color, :priority],
methods: [:text_color] methods: [:text_color]
) )
end end
......
...@@ -7,8 +7,11 @@ ...@@ -7,8 +7,11 @@
":issue-link-base" => "issueLinkBase", ":issue-link-base" => "issueLinkBase",
":disabled" => "disabled", ":disabled" => "disabled",
"track-by" => "id" } "track-by" => "id" }
%li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }", %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
":index" => "index" } ":index" => "index",
"@mousedown" => "mouseDown",
"@mouseMove" => "mouseMove",
"@mouseup" => "showIssue($event)" }
%h4.card-title %h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => "issueLinkBase + '/' + issue.id", %a{ ":href" => "issueLinkBase + '/' + issue.id",
...@@ -18,6 +21,11 @@ ...@@ -18,6 +21,11 @@
%span.card-number{ "v-if" => "issue.id" } %span.card-number{ "v-if" => "issue.id" }
= precede '#' do = precede '#' do
{{ issue.id }} {{ 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 }
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
type: "button", type: "button",
"v-if" => "(!list.label || label.id !== list.label.id)", "v-if" => "(!list.label || label.id !== list.label.id)",
...@@ -26,8 +34,3 @@ ...@@ -26,8 +34,3 @@
":title" => "label.description", ":title" => "label.description",
data: { container: 'body' } } data: { container: 'body' } }
{{ label.title }} {{ label.title }}
%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 }
%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"
.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" => "'#{root_url}' + issue.assignee.username",
"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
.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
.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), namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) },
":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"
.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
- 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.
...@@ -10,7 +10,9 @@ ...@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards = render 'shared/issuable/filter', type: :boards
.boards-list#board-app{ "v-cloak" => true, data: board_data } #board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
.boards-app-loading.text-center{ "v-if" => "loading" } .boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin") = icon("spinner spin")
= render "projects/boards/components/board" = render "projects/boards/components/board"
= render "projects/boards/components/sidebar"
...@@ -10,7 +10,9 @@ ...@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards = render 'shared/issuable/filter', type: :boards
.boards-list#board-app{ "v-cloak" => true, data: board_data } #board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
.boards-app-loading.text-center{ "v-if" => "loading" } .boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin") = icon("spinner spin")
= render "projects/boards/components/board" = render "projects/boards/components/board"
= render "projects/boards/components/sidebar"
<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
...@@ -21,9 +21,11 @@ describe Projects::Boards::IssuesController do ...@@ -21,9 +21,11 @@ describe Projects::Boards::IssuesController do
context 'with valid list id' do context 'with valid list id' do
it 'returns issues that have the list label applied' do it 'returns issues that have the list label applied' do
johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) 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: [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) create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
issue.subscribe(johndoe)
list_issues user: user, board: board, list: list2 list_issues user: user, board: board, list: list2
......
...@@ -66,6 +66,21 @@ describe 'Issue Boards new issue', feature: true, js: true do ...@@ -66,6 +66,21 @@ describe 'Issue Boards new issue', feature: true, js: true do
expect(page).to have_content('1') expect(page).to have_content('1')
end end
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 end
context 'unauthorized user' do context 'unauthorized user' do
......
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
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"iid": { "type": "integer" }, "iid": { "type": "integer" },
"title": { "type": "string" }, "title": { "type": "string" },
"confidential": { "type": "boolean" }, "confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"labels": { "labels": {
"type": "array", "type": "array",
"items": { "items": {
...@@ -42,7 +43,8 @@ ...@@ -42,7 +43,8 @@
"name": { "type": "string" }, "name": { "type": "string" },
"username": { "type": "string" }, "username": { "type": "string" },
"avatar_url": { "type": "uri" } "avatar_url": { "type": "uri" }
} },
"subscribed": { "type": ["boolean", "null"] }
}, },
"additionalProperties": false "additionalProperties": false
} }
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