From 9e1aaf163f3bd0b3c2d2aee8225d111f83b3d421 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 31 Oct 2019 02:21:50 +0200 Subject: Assignee field like Jira #2452 , in progress. Assignee can not be removed yet, it removes member, wrong link in popup. Thanks to xet7 ! --- client/components/cards/cardDetails.jade | 21 ++++ client/components/cards/cardDetails.js | 3 + client/components/cards/cardDetails.styl | 1 + client/components/users/userAvatar.jade | 32 ++++++ client/components/users/userAvatar.js | 30 ++++++ client/components/users/userAvatar.styl | 12 ++- i18n/en.i18n.json | 3 +- models/cards.js | 161 +++++++++++++++++++++++++++++++ 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 + | ({{ user.username }}) + 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( -- cgit v1.2.3-1-g7c22