summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLauri Ojansivu <x@xet7.org>2019-10-31 02:21:50 +0200
committerLauri Ojansivu <x@xet7.org>2019-10-31 02:21:50 +0200
commit9e1aaf163f3bd0b3c2d2aee8225d111f83b3d421 (patch)
tree8091ce62d88a69c2974748833e20ec6c23c6f17f
parent3f19091a913142f983e9087b069802220e29426b (diff)
downloadwekan-9e1aaf163f3bd0b3c2d2aee8225d111f83b3d421.tar.gz
wekan-9e1aaf163f3bd0b3c2d2aee8225d111f83b3d421.tar.bz2
wekan-9e1aaf163f3bd0b3c2d2aee8225d111f83b3d421.zip
Assignee field like Jira #2452 , in progress.
Assignee can not be removed yet, it removes member, wrong link in popup. Thanks to xet7 !
-rw-r--r--client/components/cards/cardDetails.jade21
-rw-r--r--client/components/cards/cardDetails.js3
-rw-r--r--client/components/cards/cardDetails.styl1
-rw-r--r--client/components/users/userAvatar.jade32
-rw-r--r--client/components/users/userAvatar.js30
-rw-r--r--client/components/users/userAvatar.styl12
-rw-r--r--i18n/en.i18n.json3
-rw-r--r--models/cards.js161
8 files changed, 258 insertions, 5 deletions
diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade
index 13b6bd13..639c7742 100644
--- a/client/components/cards/cardDetails.jade
+++ b/client/components/cards/cardDetails.jade
@@ -73,6 +73,15 @@ template(name="cardDetails")
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
i.fa.fa-plus
+ .card-details-item.card-details-item-assignees
+ h3.card-details-item-title {{_ 'assignee'}}
+ each getAssignees
+ +userAvatar(userId=this cardId=../_id)
+ | {{! XXX Hack to hide syntaxic coloration /// }}
+ if canModifyCard
+ a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
+ i.fa.fa-plus
+
.card-details-item.card-details-item-labels
h3.card-details-item-title {{_ 'labels'}}
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
@@ -296,6 +305,18 @@ template(name="cardMembersPopup")
if isCardMember
i.fa.fa-check
+template(name="cardAssigneesPopup")
+ ul.pop-over-list.js-card-assignee-list
+ each board.activeAssignees
+ li.item(class="{{#if isCardAssignee}}active{{/if}}")
+ a.name.js-select-assignee(href="#")
+ +userAvatarAssignee(userId=user._id)
+ span.full-name
+ = user.profile.fullname
+ | (<span class="username">{{ user.username }}</span>)
+ if isCardAssignee
+ i.fa.fa-check
+
template(name="cardMorePopup")
p.quiet
span.clearfix
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index 47941560..6408db74 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -309,6 +309,8 @@ BlazeComponent.extendComponent({
},
'click .js-member': Popup.open('cardMember'),
'click .js-add-members': Popup.open('cardMembers'),
+ 'click .js-assignee': Popup.open('cardAssignee'),
+ 'click .js-add-assignees': Popup.open('cardAssignees'),
'click .js-add-labels': Popup.open('cardLabels'),
'click .js-received-date': Popup.open('editCardReceivedDate'),
'click .js-start-date': Popup.open('editCardStartDate'),
@@ -399,6 +401,7 @@ Template.cardDetailsActionsPopup.helpers({
Template.cardDetailsActionsPopup.events({
'click .js-members': Popup.open('cardMembers'),
+ 'click .js-assignees': Popup.open('cardAssignees'),
'click .js-labels': Popup.open('cardLabels'),
'click .js-attachments': Popup.open('cardAttachments'),
'click .js-custom-fields': Popup.open('cardCustomFields'),
diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl
index cd475072..825e22e9 100644
--- a/client/components/cards/cardDetails.styl
+++ b/client/components/cards/cardDetails.styl
@@ -93,6 +93,7 @@
margin-right: 0
&.card-details-item-labels,
&.card-details-item-members,
+ &.card-details-item-assignees,
&.card-details-item-received,
&.card-details-item-start,
&.card-details-item-due,
diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade
index ebfa48ba..e551cab5 100644
--- a/client/components/users/userAvatar.jade
+++ b/client/components/users/userAvatar.jade
@@ -15,6 +15,23 @@ template(name="userAvatar")
a.edit-avatar.js-change-avatar
i.fa.fa-pencil
+template(name="userAvatarAssignee")
+ a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})")
+ if userData.profile.avatarUrl
+ img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}")
+ else
+ +userAvatarInitials(userId=userData._id)
+
+ if showStatus
+ span.assignee-presence-status(class=presenceStatusClassName)
+ span.assignee-type(class=assigneeType)
+
+ unless isSandstorm
+ if showEdit
+ if $eq currentUser._id userData._id
+ a.edit-avatar.js-change-avatar
+ i.fa.fa-pencil
+
template(name="userAvatarInitials")
svg.avatar.avatar-initials(viewBox="0 0 {{viewPortWidth}} 15")
text(x="50%" y="13" text-anchor="middle")= initials
@@ -78,3 +95,18 @@ template(name="cardMemberPopup")
if $eq currentUser._id user._id
with currentUser
li: a.js-edit-profile {{_ 'edit-profile'}}
+
+template(name="cardAssigneePopup")
+ .board-assignee-menu
+ .mini-profile-info
+ +userAvatar(userId=user._id showEdit=true)
+ .info
+ h3= user.profile.fullname
+ p.quiet @{{ user.username }}
+ ul.pop-over-list
+ if currentUser.isNotCommentOnly
+ li: a.js-remove-assignee {{_ 'remove-member-from-card'}}
+
+ if $eq currentUser._id user._id
+ with currentUser
+ li: a.js-edit-profile {{_ 'edit-profile'}}
diff --git a/client/components/users/userAvatar.js b/client/components/users/userAvatar.js
index 262a63af..7a2831b2 100644
--- a/client/components/users/userAvatar.js
+++ b/client/components/users/userAvatar.js
@@ -139,6 +139,13 @@ Template.cardMembersPopup.helpers({
return _.contains(cardMembers, this.userId);
},
+ isCardAssignee() {
+ const card = Template.parentData();
+ const cardAssignees = card.getAssignees();
+
+ return _.contains(cardAssignees, this.userId);
+ },
+
user() {
return Users.findOne(this.userId);
},
@@ -166,3 +173,26 @@ Template.cardMemberPopup.events({
},
'click .js-edit-profile': Popup.open('editProfile'),
});
+
+Template.cardAssigneesPopup.events({
+ 'click .js-select-assignee'(event) {
+ const card = Cards.findOne(Session.get('currentCard'));
+ const assigneeId = this.userId;
+ card.toggleAssignee(assigneeId);
+ event.preventDefault();
+ },
+});
+
+Template.cardAssigneePopup.helpers({
+ user() {
+ return Users.findOne(this.userId);
+ },
+});
+
+Template.cardAssigneePopup.events({
+ 'click .js-remove-assignee'() {
+ Cards.findOne(this.cardId).unassignAssignee(this.userId);
+ Popup.close();
+ },
+ 'click .js-edit-profile': Popup.open('editProfile'),
+});
diff --git a/client/components/users/userAvatar.styl b/client/components/users/userAvatar.styl
index b962b01c..5fcd9f6c 100644
--- a/client/components/users/userAvatar.styl
+++ b/client/components/users/userAvatar.styl
@@ -2,7 +2,8 @@
avatar-radius = 50%
-.member
+.member,
+.assignee
border-radius: 3px
display: block
position: relative
@@ -32,7 +33,8 @@ avatar-radius = 50%
height: 100%
width: @height
- .member-presence-status
+ .member-presence-status,
+ .assignee-presence-status
background-color: #b3b3b3
border: 1px solid #fff
border-radius: 50%
@@ -79,7 +81,8 @@ avatar-radius = 50%
color: white
- &.add-member
+ &.add-member,
+ &.add-assignee
display: flex
align-items: center
justify-content: center
@@ -111,7 +114,8 @@ avatar-radius = 50%
p
padding-top: 0
- .member
+ .member,
+ .assignee
width: 50px
height: @width
margin-right: 10px
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index dd8b7130..5a595696 100644
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -750,5 +750,6 @@
"delete-user-confirm-popup": "Are you sure you want to delete this account? There is no undo.",
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
- "show-desktop-drag-handles": "Show desktop drag handles"
+ "show-desktop-drag-handles": "Show desktop drag handles",
+ "assignee": "Assignee"
}
diff --git a/models/cards.js b/models/cards.js
index 1c56a6d2..78005b38 100644
--- a/models/cards.js
+++ b/models/cards.js
@@ -203,6 +203,14 @@ Cards.attachSchema(
optional: true,
defaultValue: [],
},
+ assignees: {
+ /**
+ * who assignees of the card (user IDs)
+ */
+ type: [String],
+ optional: true,
+ defaultValue: [],
+ },
receivedAt: {
/**
* Date the card was received
@@ -411,6 +419,10 @@ Cards.helpers({
return _.contains(this.getMembers(), memberId);
},
+ isAssignee(assigneeId) {
+ return _.contains(this.getAssignees(), assigneeId);
+ },
+
activities() {
if (this.isLinkedCard()) {
return Activities.find(
@@ -745,6 +757,20 @@ Cards.helpers({
}
},
+ getAssignees() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({ _id: this.linkedId });
+ return card.assignees;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId });
+ return board.activeAssignees().map(assignee => {
+ return assignee.userId;
+ });
+ } else {
+ return this.assignees;
+ }
+ },
+
assignMember(memberId) {
if (this.isLinkedCard()) {
return Cards.update(
@@ -762,6 +788,23 @@ Cards.helpers({
}
},
+ assignAssignee(assigneeId) {
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ { $addToSet: { assignees: assigneeId } },
+ );
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId });
+ return board.addAssignee(assigneeId);
+ } else {
+ return Cards.update(
+ { _id: this._id },
+ { $addToSet: { assignees: assigneeId } },
+ );
+ }
+ },
+
unassignMember(memberId) {
if (this.isLinkedCard()) {
return Cards.update(
@@ -776,6 +819,23 @@ Cards.helpers({
}
},
+ unassignAssignee(assigneeId) {
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ { $pull: { assignees: assigneeId } },
+ );
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId });
+ return board.removeAssignee(assigneeId);
+ } else {
+ return Cards.update(
+ { _id: this._id },
+ { $pull: { assignees: assigneeId } },
+ );
+ }
+ },
+
toggleMember(memberId) {
if (this.getMembers() && this.getMembers().indexOf(memberId) > -1) {
return this.unassignMember(memberId);
@@ -784,6 +844,14 @@ Cards.helpers({
}
},
+ toggleAssignee(assigneeId) {
+ if (this.getAssignees() && this.getAssignees().indexOf(assigneeId) > -1) {
+ return this.unassignAssignee(assigneeId);
+ } else {
+ return this.assignAssignee(assigneeId);
+ }
+ },
+
getReceived() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
@@ -1126,6 +1194,14 @@ Cards.mutations({
};
},
+ assignAssignee(assigneeId) {
+ return {
+ $addToSet: {
+ assignees: assigneeId,
+ },
+ };
+ },
+
unassignMember(memberId) {
return {
$pull: {
@@ -1134,6 +1210,14 @@ Cards.mutations({
};
},
+ unassignAssignee(assigneeId) {
+ return {
+ $pull: {
+ assignees: assigneeId,
+ },
+ };
+ },
+
toggleMember(memberId) {
if (this.members && this.members.indexOf(memberId) > -1) {
return this.unassignMember(memberId);
@@ -1142,6 +1226,14 @@ Cards.mutations({
}
},
+ toggleAssignee(assigneeId) {
+ if (this.assignees && this.assignees.indexOf(assigneeId) > -1) {
+ return this.unassignAssignee(assigneeId);
+ } else {
+ return this.assignAssignee(assigneeId);
+ }
+ },
+
assignCustomField(customFieldId) {
return {
$addToSet: {
@@ -1436,6 +1528,46 @@ function cardMembers(userId, doc, fieldNames, modifier) {
}
}
+function cardAssignees(userId, doc, fieldNames, modifier) {
+ if (!_.contains(fieldNames, 'assignees')) return;
+ let assigneeId;
+ // Say hello to the new assignee
+ if (modifier.$addToSet && modifier.$addToSet.assignees) {
+ assigneeId = modifier.$addToSet.assignees;
+ const username = Users.findOne(assigneeId).username;
+ if (!_.contains(doc.assignees, assigneeId)) {
+ Activities.insert({
+ userId,
+ username,
+ activityType: 'joinAssignee',
+ boardId: doc.boardId,
+ cardId: doc._id,
+ assigneeId,
+ listId: doc.listId,
+ swimlaneId: doc.swimlaneId,
+ });
+ }
+ }
+ // Say goodbye to the former assignee
+ if (modifier.$pull && modifier.$pull.assignees) {
+ assigneeId = modifier.$pull.assignees;
+ const username = Users.findOne(assigneeId).username;
+ // Check that the former assignee is assignee of the card
+ if (_.contains(doc.assignees, assigneeId)) {
+ Activities.insert({
+ userId,
+ username,
+ activityType: 'unjoinAssignee',
+ boardId: doc.boardId,
+ cardId: doc._id,
+ assigneeId,
+ listId: doc.listId,
+ swimlaneId: doc.swimlaneId,
+ });
+ }
+ }
+}
+
function cardLabels(userId, doc, fieldNames, modifier) {
if (!_.contains(fieldNames, 'labelIds')) return;
let labelId;
@@ -1673,6 +1805,12 @@ if (Meteor.isServer) {
updateActivities(doc, fieldNames, modifier);
});
+ // Add a new activity if we add or remove a assignee to the card
+ Cards.before.update((userId, doc, fieldNames, modifier) => {
+ cardAssignees(userId, doc, fieldNames, modifier);
+ updateActivities(doc, fieldNames, modifier);
+ });
+
// Add a new activity if we add or remove a label to the card
Cards.before.update((userId, doc, fieldNames, modifier) => {
cardLabels(userId, doc, fieldNames, modifier);
@@ -1852,6 +1990,7 @@ if (Meteor.isServer) {
* @param {string} description the description of the new card
* @param {string} swimlaneId the swimlane ID of the new card
* @param {string} [members] the member IDs list of the new card
+ * @param {string} [assignees] the assignee IDs list of the new card
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function(
@@ -1873,6 +2012,7 @@ if (Meteor.isServer) {
_id: req.body.authorId,
});
const members = req.body.members || [req.body.authorId];
+ const assignees = req.body.assignees;
if (typeof check !== 'undefined') {
const id = Cards.direct.insert({
title: req.body.title,
@@ -1884,6 +2024,7 @@ if (Meteor.isServer) {
swimlaneId: req.body.swimlaneId,
sort: currentCards.count(),
members,
+ assignees,
});
JsonRoutes.sendResult(res, {
code: 200,
@@ -1935,6 +2076,7 @@ if (Meteor.isServer) {
* @param {string} [labelIds] the new list of label IDs attached to the card
* @param {string} [swimlaneId] the new swimlane ID of the card
* @param {string} [members] the new list of member IDs attached to the card
+ * @param {string} [assignees] the new list of assignee IDs attached to the card
* @param {string} [requestedBy] the new requestedBy field of the card
* @param {string} [assignedBy] the new assignedBy field of the card
* @param {string} [receivedAt] the new receivedAt field of the card
@@ -2195,6 +2337,25 @@ if (Meteor.isServer) {
{ $set: { members: newmembers } },
);
}
+ if (req.body.hasOwnProperty('assignees')) {
+ let newassignees = req.body.assignees;
+ if (_.isString(newassignees)) {
+ if (newassignees === '') {
+ newassignees = null;
+ } else {
+ newassignees = [newassignees];
+ }
+ }
+ Cards.direct.update(
+ {
+ _id: paramCardId,
+ listId: paramListId,
+ boardId: paramBoardId,
+ archived: false,
+ },
+ { $set: { assignees: newassignees } },
+ );
+ }
if (req.body.hasOwnProperty('swimlaneId')) {
const newParamSwimlaneId = req.body.swimlaneId;
Cards.direct.update(