Commit 862e0eb5 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'multiple-assignees-issue-board-sidebar' into 'multiple_assignees_review'

Add multiple assignees to issue board sidebar

See merge request !1731
parents 973f7588 a2bebc74
...@@ -12,7 +12,7 @@ require('./models/issue'); ...@@ -12,7 +12,7 @@ require('./models/issue');
require('./models/label'); require('./models/label');
require('./models/list'); require('./models/list');
require('./models/milestone'); require('./models/milestone');
require('./models/user'); require('./models/assignee');
require('./stores/boards_store'); require('./stores/boards_store');
require('./stores/modal_store'); require('./stores/modal_store');
require('./services/board_service'); require('./services/board_service');
......
...@@ -5,6 +5,10 @@ ...@@ -5,6 +5,10 @@
/* global Sidebar */ /* global Sidebar */
import Vue from 'vue'; import Vue from 'vue';
import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
require('./sidebar/remove_issue'); require('./sidebar/remove_issue');
...@@ -23,6 +27,7 @@ require('./sidebar/remove_issue'); ...@@ -23,6 +27,7 @@ require('./sidebar/remove_issue');
detail: Store.detail, detail: Store.detail,
issue: {}, issue: {},
list: {}, list: {},
loadingAssignees: false,
}; };
}, },
computed: { computed: {
...@@ -41,6 +46,10 @@ require('./sidebar/remove_issue'); ...@@ -41,6 +46,10 @@ require('./sidebar/remove_issue');
this.issue = this.detail.issue; this.issue = this.detail.issue;
this.list = this.detail.list; this.list = this.detail.list;
this.$nextTick(() => {
this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate;
});
}, },
deep: true deep: true
}, },
...@@ -56,7 +65,46 @@ require('./sidebar/remove_issue'); ...@@ -56,7 +65,46 @@ require('./sidebar/remove_issue');
methods: { methods: {
closeSidebar () { closeSidebar () {
this.detail.issue = {}; this.detail.issue = {};
} },
assignSelf () {
// Notify gl dropdown that we are now assigning to current user
this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself'));
this.addAssignee(this.currentUser);
this.saveAssignees();
},
removeAssignee (a) {
gl.issueBoards.BoardsStore.detail.issue.removeAssignee(a);
},
addAssignee (a) {
gl.issueBoards.BoardsStore.detail.issue.addAssignee(a);
},
removeAllAssignees () {
gl.issueBoards.BoardsStore.detail.issue.removeAllAssignees();
},
saveAssignees () {
this.loadingAssignees = true;
gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint)
.then(() => this.loadingAssignees = false)
.catch(() => {
this.loadingAssignees = false;
return new Flash('An error occurred while saving assignees');
});
},
},
created () {
// Get events from glDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
},
beforeDestroy() {
eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
eventHub.$off('sidebar.addAssignee', this.addAssignee);
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
}, },
mounted () { mounted () {
new IssuableContext(this.currentUser); new IssuableContext(this.currentUser);
...@@ -68,6 +116,8 @@ require('./sidebar/remove_issue'); ...@@ -68,6 +116,8 @@ require('./sidebar/remove_issue');
}, },
components: { components: {
removeBtn: gl.issueBoards.RemoveIssueBtn, removeBtn: gl.issueBoards.RemoveIssueBtn,
'assignee-title': AssigneeTitle,
assignees: Assignees,
}, },
}); });
})(); })();
...@@ -158,7 +158,7 @@ import eventHub from '../eventhub'; ...@@ -158,7 +158,7 @@ import eventHub from '../eventhub';
> >
<img <img
class="avatar avatar-inline s20" class="avatar avatar-inline s20"
:src="assignee.avatar" :src="assignee.avatarUrl"
width="20" width="20"
height="20" height="20"
:alt="avatarUrlTitle(assignee)" :alt="avatarUrlTitle(assignee)"
......
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
class ListUser { class ListAssignee {
constructor(user) { constructor(user) {
this.id = user.id; this.id = user.id;
this.name = user.name; this.name = user.name;
this.username = user.username; this.username = user.username;
this.avatar = user.avatar_url; this.avatarUrl = user.avatar_url;
} }
} }
window.ListUser = ListUser; window.ListAssignee = ListAssignee;
/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */ /* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
/* global ListLabel */ /* global ListLabel */
/* global ListMilestone */ /* global ListMilestone */
/* global ListUser */ /* global ListAssignee */
import Vue from 'vue'; import Vue from 'vue';
...@@ -14,6 +14,7 @@ class ListIssue { ...@@ -14,6 +14,7 @@ class ListIssue {
this.dueDate = obj.due_date; this.dueDate = obj.due_date;
this.subscribed = obj.subscribed; this.subscribed = obj.subscribed;
this.labels = []; this.labels = [];
this.assignees = [];
this.selected = false; this.selected = false;
this.position = obj.relative_position || Infinity; this.position = obj.relative_position || Infinity;
this.milestone_id = obj.milestone_id; this.milestone_id = obj.milestone_id;
...@@ -26,7 +27,7 @@ class ListIssue { ...@@ -26,7 +27,7 @@ class ListIssue {
this.labels.push(new ListLabel(label)); this.labels.push(new ListLabel(label));
}); });
this.assignees = obj.assignees.map(a => new ListUser(a)); this.assignees = obj.assignees.map(a => new ListAssignee(a));
} }
addLabel (label) { addLabel (label) {
...@@ -49,6 +50,26 @@ class ListIssue { ...@@ -49,6 +50,26 @@ class ListIssue {
labels.forEach(this.removeLabel.bind(this)); labels.forEach(this.removeLabel.bind(this));
} }
addAssignee (assignee) {
if (!this.findAssignee(assignee)) {
this.assignees.push(new ListAssignee(assignee));
}
}
findAssignee (findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
}
removeAssignee (removeAssignee) {
if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
}
}
removeAllAssignees () {
this.assignees = [];
}
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));
} }
...@@ -58,7 +79,7 @@ class ListIssue { ...@@ -58,7 +79,7 @@ class ListIssue {
issue: { issue: {
milestone_id: this.milestone ? this.milestone.id : null, milestone_id: this.milestone ? this.milestone.id : null,
due_date: this.dueDate, due_date: this.dueDate,
assignee_id: this.assignee ? this.assignee.id : null, assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0],
label_ids: this.labels.map((label) => label.id) label_ids: this.labels.map((label) => label.id)
} }
}; };
......
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
/* global Issuable */ /* global Issuable */
/* global ListUser */
import Vue from 'vue';
import eventHub from './sidebar/event_hub'; import eventHub from './sidebar/event_hub';
(function() { (function() {
...@@ -81,14 +79,6 @@ import eventHub from './sidebar/event_hub'; ...@@ -81,14 +79,6 @@ import eventHub from './sidebar/event_hub';
.get(); .get();
}; };
var updateIssueBoardsIssue = function () {
$loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
$loading.fadeOut();
});
};
$('.assign-to-me-link').on('click', (e) => { $('.assign-to-me-link').on('click', (e) => {
e.preventDefault(); e.preventDefault();
$(e.currentTarget).hide(); $(e.currentTarget).hide();
...@@ -98,22 +88,11 @@ import eventHub from './sidebar/event_hub'; ...@@ -98,22 +88,11 @@ import eventHub from './sidebar/event_hub';
$dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default'); $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default');
}); });
$block.on('click', '.js-assign-yourself', function(e) { $block.on('click', '.js-assign-yourself', (e) => {
e.preventDefault(); 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) { assignTo = function(selected) {
var data; var data;
data = {}; data = {};
...@@ -289,7 +268,6 @@ import eventHub from './sidebar/event_hub'; ...@@ -289,7 +268,6 @@ import eventHub from './sidebar/event_hub';
return $value.css('display', ''); return $value.css('display', '');
}, },
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) { clicked: function(options) {
const { $el, e, isMarking } = options; const { $el, e, isMarking } = options;
const user = options.selectedObj; const user = options.selectedObj;
...@@ -367,19 +345,6 @@ import eventHub from './sidebar/event_hub'; ...@@ -367,19 +345,6 @@ import eventHub from './sidebar/event_hub';
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 if (!$dropdown.hasClass('js-multiselect')) { } else if (!$dropdown.hasClass('js-multiselect')) {
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);
......
.block.assignee .block.assignee{ ref: "assigneeBlock" }
.title.hide-collapsed %template{ "v-if" => "issue.assignees"}
Assignee %assignee-title{ ":number-of-assignees" => "issue.assignees.length",
- if can?(current_user, :admin_issue, @project) ":loading" => "loadingAssignees",
= icon("spinner spin", class: "block-loading") ":editable" => can?(current_user, :admin_issue, @project) }
= link_to "Edit", "#", class: "edit-link pull-right" %assignees{ class: "value", "root-path" => "#{root_url}",
.value.hide-collapsed ":users" => "issue.assignees",
%span.assign-yourself.no-value{ "v-if" => "!issue.assignee" } "@assign-self" => "assignSelf" }
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", alt: "Avatar" }
%span.author
{{ issue.assignee.name }}
%span.username
= precede "@" do
{{ issue.assignee.username }}
- if can?(current_user, :admin_issue, @project) - if can?(current_user, :admin_issue, @project)
.selectbox.hide-collapsed .selectbox.hide-collapsed
%input{ type: "hidden", %input{ type: "hidden",
name: "issue[assignee_id]", name: "issue[assignee_ids][]",
id: "issue_assignee_id", ":value" => "assignee.id",
":value" => "issue.assignee.id", "v-if" => "issue.assignees",
"v-if" => "issue.assignee" } "v-for" => "assignee in issue.assignees" }
.dropdown .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" }, %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", dropdown: { header: 'Assignee(s)'} },
":data-issuable-id" => "issue.id", ":data-issuable-id" => "issue.id",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Select assignee Select assignee(s)
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
= dropdown_title("Assign to") = dropdown_title("Assign to")
= dropdown_filter("Search users") = dropdown_filter("Search users")
= dropdown_content = dropdown_content
......
/* global ListUser */ /* global ListAssignee */
/* global ListLabel */ /* global ListLabel */
/* global listObj */ /* global listObj */
/* global ListIssue */ /* global ListIssue */
...@@ -14,7 +14,7 @@ require('~/boards/components/issue_card_inner'); ...@@ -14,7 +14,7 @@ require('~/boards/components/issue_card_inner');
require('./mock_data'); require('./mock_data');
describe('Issue card component', () => { describe('Issue card component', () => {
const user = new ListUser({ const user = new ListAssignee({
id: 1, id: 1,
name: 'testing 123', name: 'testing 123',
username: 'test', username: 'test',
......
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