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
This commit is contained in:
commit
a66a3c7449
35 changed files with 913 additions and 48 deletions
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
(() => {
|
||||
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');
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,4 @@
|
|||
Vue.filter('due-date', (value) => {
|
||||
const date = new Date(value);
|
||||
return $.datepicker.formatDate('M d, yy', date);
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
6
app/assets/javascripts/boards/models/milestone.js.es6
Normal file
6
app/assets/javascripts/boards/models/milestone.js.es6
Normal file
|
@ -0,0 +1,6 @@
|
|||
class ListMilestone {
|
||||
constructor (obj) {
|
||||
this.id = obj.id;
|
||||
this.title = obj.title;
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
gl.issueBoards.BoardsStore = {
|
||||
disabled: false,
|
||||
state: {},
|
||||
detail: {
|
||||
issue: {}
|
||||
},
|
||||
moving: {
|
||||
issue: {},
|
||||
list: {}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -622,6 +622,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) {
|
||||
|
|
|
@ -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')) {
|
||||
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
@ -212,6 +229,10 @@
|
|||
margin-right: 5px;
|
||||
font-size: (14px / $issue-boards-font-size) * 1em;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-number {
|
||||
|
@ -264,3 +285,48 @@
|
|||
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
|
|||
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
|
||||
|
|
|
@ -287,10 +287,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
|
||||
|
|
|
@ -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",
|
||||
|
@ -18,6 +21,11 @@
|
|||
%span.card-number{ "v-if" => "issue.id" }
|
||||
= precede '#' do
|
||||
{{ issue.id }}
|
||||
%a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
|
||||
":title" => "'Assigned to ' + issue.assignee.name",
|
||||
"v-if" => "issue.assignee",
|
||||
data: { container: 'body' } }
|
||||
%img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
|
||||
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
|
||||
type: "button",
|
||||
"v-if" => "(!list.label || label.id !== list.label.id)",
|
||||
|
@ -26,8 +34,3 @@
|
|||
":title" => "label.description",
|
||||
data: { container: 'body' } }
|
||||
{{ 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 }
|
||||
|
|
23
app/views/projects/boards/components/_sidebar.html.haml
Normal file
23
app/views/projects/boards/components/_sidebar.html.haml
Normal file
|
@ -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"
|
|
@ -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" => "'#{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
|
|
@ -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
|
|
@ -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), 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"
|
|
@ -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
|
|
@ -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.
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
1
app/views/shared/icons/_icon_close.svg
Normal file
1
app/views/shared/icons/_icon_close.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 322 B |
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
312
spec/features/boards/sidebar_spec.rb
Normal file
312
spec/features/boards/sidebar_spec.rb
Normal file
|
@ -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
|
4
spec/fixtures/api/schemas/issue.json
vendored
4
spec/fixtures/api/schemas/issue.json
vendored
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue