From 3e8f9ef1a5275a5e9b691c7e74dc73b97a43689a Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sat, 2 Nov 2019 16:12:40 +0200 Subject: 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 ! --- client/components/cards/cardDetails.jade | 40 ++++++++++- client/components/cards/cardDetails.js | 104 +++++++++++++++++++++++++++ client/components/cards/cardDetails.styl | 120 +++++++++++++++++++++++++++++++ client/components/users/userAvatar.jade | 32 --------- client/components/users/userAvatar.js | 30 -------- client/components/users/userAvatar.styl | 12 ++-- 6 files changed, 266 insertions(+), 72 deletions(-) (limited to 'client') diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index 639c7742..ad8010e4 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -76,7 +76,7 @@ template(name="cardDetails") .card-details-item.card-details-item-assignees h3.card-details-item-title {{_ 'assignee'}} each getAssignees - +userAvatar(userId=this cardId=../_id) + +userAvatarAssignee(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'}}") @@ -307,7 +307,7 @@ template(name="cardMembersPopup") template(name="cardAssigneesPopup") ul.pop-over-list.js-card-assignee-list - each board.activeAssignees + each board.activeMembers li.item(class="{{#if isCardAssignee}}active{{/if}}") a.name.js-select-assignee(href="#") +userAvatarAssignee(userId=user._id) @@ -317,6 +317,42 @@ template(name="cardAssigneesPopup") if isCardAssignee 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") p.quiet span.clearfix diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 6408db74..3b2873a2 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -344,6 +344,50 @@ BlazeComponent.extendComponent({ }, }).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 // feature. (class extends InlinedForm { @@ -809,3 +853,63 @@ EscapeActions.register( 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'), +}); diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl index 825e22e9..295a659d 100644 --- a/client/components/cards/cardDetails.styl +++ b/client/components/cards/cardDetails.styl @@ -1,5 +1,125 @@ @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 padding: 0 flex-shrink: 0 diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade index e551cab5..ebfa48ba 100644 --- a/client/components/users/userAvatar.jade +++ b/client/components/users/userAvatar.jade @@ -15,23 +15,6 @@ 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 @@ -95,18 +78,3 @@ 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 7a2831b2..262a63af 100644 --- a/client/components/users/userAvatar.js +++ b/client/components/users/userAvatar.js @@ -139,13 +139,6 @@ 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); }, @@ -173,26 +166,3 @@ 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 5fcd9f6c..b962b01c 100644 --- a/client/components/users/userAvatar.styl +++ b/client/components/users/userAvatar.styl @@ -2,8 +2,7 @@ avatar-radius = 50% -.member, -.assignee +.member border-radius: 3px display: block position: relative @@ -33,8 +32,7 @@ avatar-radius = 50% height: 100% width: @height - .member-presence-status, - .assignee-presence-status + .member-presence-status background-color: #b3b3b3 border: 1px solid #fff border-radius: 50% @@ -81,8 +79,7 @@ avatar-radius = 50% color: white - &.add-member, - &.add-assignee + &.add-member display: flex align-items: center justify-content: center @@ -114,8 +111,7 @@ avatar-radius = 50% p padding-top: 0 - .member, - .assignee + .member width: 50px height: @width margin-right: 10px -- cgit v1.2.3-1-g7c22