Преглед на файлове

Assignee field like Jira #2452 , in progress.
Assignee can not be removed yet, it removes member, wrong link in popup.

Thanks to xet7 !

Lauri Ojansivu преди 5 години
родител
ревизия
9e1aaf163f

+ 21 - 0
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

+ 3 - 0
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'),

+ 1 - 0
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,

+ 32 - 0
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'}}

+ 30 - 0
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'),
+});

+ 8 - 4
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

+ 2 - 1
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"
 }

+ 161 - 0
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(