Explorar o código

Assignee field like Jira #2452 , in progress.

Added features:
- Assignee can now be added and removed.
- Avatar icon is at card and assignee details

TODO:
- When selecting new assignee (+) icon, list does not yet show avatars and names who to add.
  There is empty avatar without name.

Thanks to xet7 !
Lauri Ojansivu %!s(int64=5) %!d(string=hai) anos
pai
achega
3e8f9ef1a5

+ 38 - 2
client/components/cards/cardDetails.jade

@@ -76,7 +76,7 @@ template(name="cardDetails")
       .card-details-item.card-details-item-assignees
       .card-details-item.card-details-item-assignees
         h3.card-details-item-title {{_ 'assignee'}}
         h3.card-details-item-title {{_ 'assignee'}}
         each getAssignees
         each getAssignees
-          +userAvatar(userId=this cardId=../_id)
+          +userAvatarAssignee(userId=this cardId=../_id)
           | {{! XXX Hack to hide syntaxic coloration /// }}
           | {{! XXX Hack to hide syntaxic coloration /// }}
         if canModifyCard
         if canModifyCard
           a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
           a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
@@ -307,7 +307,7 @@ template(name="cardMembersPopup")
 
 
 template(name="cardAssigneesPopup")
 template(name="cardAssigneesPopup")
   ul.pop-over-list.js-card-assignee-list
   ul.pop-over-list.js-card-assignee-list
-    each board.activeAssignees
+    each board.activeMembers
       li.item(class="{{#if isCardAssignee}}active{{/if}}")
       li.item(class="{{#if isCardAssignee}}active{{/if}}")
         a.name.js-select-assignee(href="#")
         a.name.js-select-assignee(href="#")
           +userAvatarAssignee(userId=user._id)
           +userAvatarAssignee(userId=user._id)
@@ -317,6 +317,42 @@ template(name="cardAssigneesPopup")
           if isCardAssignee
           if isCardAssignee
             i.fa.fa-check
             i.fa.fa-check
 
 
+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
+      +userAvatarAssigneeInitials(userId=userData._id)
+
+    if showStatus
+      span.member-presence-status(class=presenceStatusClassName)
+      span.member-type(class=memberType)
+
+    unless isSandstorm
+      if showEdit
+        if $eq currentUser._id userData._id
+          a.edit-avatar.js-change-avatar
+            i.fa.fa-pencil
+
+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'}}
+
+template(name="userAvatarAssigneeInitials")
+  svg.avatar.avatar-assignee-initials(viewBox="0 0 {{viewPortWidth}} 15")
+    text(x="50%" y="13" text-anchor="middle")= initials
+
 template(name="cardMorePopup")
 template(name="cardMorePopup")
   p.quiet
   p.quiet
     span.clearfix
     span.clearfix

+ 104 - 0
client/components/cards/cardDetails.js

@@ -344,6 +344,50 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('cardDetails');
 }).register('cardDetails');
 
 
+Template.cardDetails.helpers({
+  userData() {
+    // We need to handle a special case for the search results provided by the
+    // `matteodem:easy-search` package. Since these results gets published in a
+    // separate collection, and not in the standard Meteor.Users collection as
+    // expected, we use a component parameter ("property") to distinguish the
+    // two cases.
+    const userCollection = this.esSearch ? ESSearchResults : Users;
+    return userCollection.findOne(this.userId, {
+      fields: {
+        profile: 1,
+        username: 1,
+      },
+    });
+  },
+
+  memberType() {
+    const user = Users.findOne(this.userId);
+    return user && user.isBoardAdmin() ? 'admin' : 'normal';
+  },
+
+  presenceStatusClassName() {
+    const user = Users.findOne(this.userId);
+    const userPresence = presences.findOne({ userId: this.userId });
+    if (user && user.isInvitedTo(Session.get('currentBoard'))) return 'pending';
+    else if (!userPresence) return 'disconnected';
+    else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
+      return 'active';
+    else return 'idle';
+  },
+});
+
+Template.userAvatarAssigneeInitials.helpers({
+  initials() {
+    const user = Users.findOne(this.userId);
+    return user && user.getInitials();
+  },
+
+  viewPortWidth() {
+    const user = Users.findOne(this.userId);
+    return ((user && user.getInitials().length) || 1) * 12;
+  },
+});
+
 // We extends the normal InlinedForm component to support UnsavedEdits draft
 // We extends the normal InlinedForm component to support UnsavedEdits draft
 // feature.
 // feature.
 (class extends InlinedForm {
 (class extends InlinedForm {
@@ -809,3 +853,63 @@ EscapeActions.register(
     noClickEscapeOn: '.js-card-details,.board-sidebar,#header',
     noClickEscapeOn: '.js-card-details,.board-sidebar,#header',
   },
   },
 );
 );
+
+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({
+  userData() {
+    // We need to handle a special case for the search results provided by the
+    // `matteodem:easy-search` package. Since these results gets published in a
+    // separate collection, and not in the standard Meteor.Users collection as
+    // expected, we use a component parameter ("property") to distinguish the
+    // two cases.
+    const userCollection = this.esSearch ? ESSearchResults : Users;
+    return userCollection.findOne(this.userId, {
+      fields: {
+        profile: 1,
+        username: 1,
+      },
+    });
+  },
+
+  memberType() {
+    const user = Users.findOne(this.userId);
+    return user && user.isBoardAdmin() ? 'admin' : 'normal';
+  },
+
+  presenceStatusClassName() {
+    const user = Users.findOne(this.userId);
+    const userPresence = presences.findOne({ userId: this.userId });
+    if (user && user.isInvitedTo(Session.get('currentBoard'))) return 'pending';
+    else if (!userPresence) return 'disconnected';
+    else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
+      return 'active';
+    else return 'idle';
+  },
+
+  isCardAssignee() {
+    const card = Template.parentData();
+    const cardAssignees = card.getAssignees();
+
+    return _.contains(cardAssignees, this.userId);
+  },
+
+  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'),
+});

+ 120 - 0
client/components/cards/cardDetails.styl

@@ -1,5 +1,125 @@
 @import 'nib'
 @import 'nib'
 
 
+// Assignee, code copied from wekan/client/users/userAvatar.styl
+
+avatar-radius = 50%
+
+.assignee
+  border-radius: 3px
+  display: block
+  position: relative
+  float: left
+  height: 30px
+  width: @height
+  margin: 0 4px 4px 0
+  cursor: pointer
+  user-select: none
+  z-index: 1
+  text-decoration: none
+  border-radius: avatar-radius
+
+  .avatar
+    overflow: hidden
+    border-radius: avatar-radius
+
+    &.avatar-assignee-initials
+      height: 70%
+      width: @height
+      padding: 15%
+      background-color: #dbdbdb
+      color: #444444
+      position: absolute
+
+    &.avatar-image
+      height: 100%
+      width: @height
+
+  .assignee-presence-status
+    background-color: #b3b3b3
+    border: 1px solid #fff
+    border-radius: 50%
+    height: 7px
+    width: @height
+    position: absolute
+    right: -1px
+    bottom: -1px
+    border: 1px solid white
+    z-index: 15
+
+    &.active
+      background: #64c464
+      border-color: #daf1da
+
+    &.idle
+      background: #e4e467
+      border-color: #f7f7d4
+
+    &.disconnected
+      background: #bdbdbd
+      border-color: #ededed
+
+    &.pending
+      background: #e44242
+      border-color: #f1dada
+
+  .edit-avatar
+    position: absolute
+    top: 0
+    height: 100%
+    width: 100%
+    border-radius: avatar-radius
+    background: black
+    display: flex
+    align-items: center
+    justify-content: center
+    opacity: 0
+
+    &:hover
+      opacity: 0.6
+
+    i.fa-pencil
+      color: white
+
+
+  &.add-assignee
+    display: flex
+    align-items: center
+    justify-content: center
+    box-shadow: 0 0 0 2px darken(white, 25%) inset
+
+    &:hover, &.is-active
+      box-shadow: 0 0 0 2px darken(white, 60%) inset
+
+.atMention
+  background: #dbdbdb
+  border-radius: 3px
+  padding: 1px 4px
+  margin: -1px 0
+  display: inline-block
+
+  &.me
+    background: #cfdfe8
+
+.mini-profile-info
+  margin-top: 10px
+
+  .info
+    padding-top: 5px
+
+    h3, p
+      margin-bottom: 0
+      padding-left: 0
+
+    p
+      padding-top: 0
+
+  .assignee
+    width: 50px
+    height: @width
+    margin-right: 10px
+
+// Other card details
+
 .card-details
 .card-details
   padding: 0
   padding: 0
   flex-shrink: 0
   flex-shrink: 0

+ 0 - 32
client/components/users/userAvatar.jade

@@ -15,23 +15,6 @@ template(name="userAvatar")
           a.edit-avatar.js-change-avatar
           a.edit-avatar.js-change-avatar
             i.fa.fa-pencil
             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")
 template(name="userAvatarInitials")
   svg.avatar.avatar-initials(viewBox="0 0 {{viewPortWidth}} 15")
   svg.avatar.avatar-initials(viewBox="0 0 {{viewPortWidth}} 15")
     text(x="50%" y="13" text-anchor="middle")= initials
     text(x="50%" y="13" text-anchor="middle")= initials
@@ -95,18 +78,3 @@ template(name="cardMemberPopup")
       if $eq currentUser._id user._id
       if $eq currentUser._id user._id
         with currentUser
         with currentUser
           li: a.js-edit-profile {{_ 'edit-profile'}}
           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'}}

+ 0 - 30
client/components/users/userAvatar.js

@@ -139,13 +139,6 @@ Template.cardMembersPopup.helpers({
     return _.contains(cardMembers, this.userId);
     return _.contains(cardMembers, this.userId);
   },
   },
 
 
-  isCardAssignee() {
-    const card = Template.parentData();
-    const cardAssignees = card.getAssignees();
-
-    return _.contains(cardAssignees, this.userId);
-  },
-
   user() {
   user() {
     return Users.findOne(this.userId);
     return Users.findOne(this.userId);
   },
   },
@@ -173,26 +166,3 @@ Template.cardMemberPopup.events({
   },
   },
   'click .js-edit-profile': Popup.open('editProfile'),
   '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'),
-});

+ 4 - 8
client/components/users/userAvatar.styl

@@ -2,8 +2,7 @@
 
 
 avatar-radius = 50%
 avatar-radius = 50%
 
 
-.member,
-.assignee
+.member
   border-radius: 3px
   border-radius: 3px
   display: block
   display: block
   position: relative
   position: relative
@@ -33,8 +32,7 @@ avatar-radius = 50%
       height: 100%
       height: 100%
       width: @height
       width: @height
 
 
-  .member-presence-status,
-  .assignee-presence-status
+  .member-presence-status
     background-color: #b3b3b3
     background-color: #b3b3b3
     border: 1px solid #fff
     border: 1px solid #fff
     border-radius: 50%
     border-radius: 50%
@@ -81,8 +79,7 @@ avatar-radius = 50%
       color: white
       color: white
 
 
 
 
-  &.add-member,
-  &.add-assignee
+  &.add-member
     display: flex
     display: flex
     align-items: center
     align-items: center
     justify-content: center
     justify-content: center
@@ -114,8 +111,7 @@ avatar-radius = 50%
     p
     p
       padding-top: 0
       padding-top: 0
 
 
-  .member,
-  .assignee
+  .member
     width: 50px
     width: 50px
     height: @width
     height: @width
     margin-right: 10px
     margin-right: 10px

+ 1 - 1
models/cards.js

@@ -763,7 +763,7 @@ Cards.helpers({
       return card.assignees;
       return card.assignees;
     } else if (this.isLinkedBoard()) {
     } else if (this.isLinkedBoard()) {
       const board = Boards.findOne({ _id: this.linkedId });
       const board = Boards.findOne({ _id: this.linkedId });
-      return board.activeAssignees().map(assignee => {
+      return board.activeMembers().map(assignee => {
         return assignee.userId;
         return assignee.userId;
       });
       });
     } else {
     } else {