mirror of
https://github.com/wekan/wekan.git
synced 2025-09-22 01:50:48 +02:00
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 !
This commit is contained in:
parent
92efb8bec4
commit
3e8f9ef1a5
7 changed files with 267 additions and 73 deletions
|
@ -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
|
||||||
|
|
|
@ -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'),
|
||||||
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'}}
|
|
||||||
|
|
|
@ -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'),
|
|
||||||
});
|
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
avatar-radius = 50%
|
avatar-radius = 50%
|
||||||
|
|
||||||
.member,
|
.member
|
||||||
.assignee
|
|
||||||
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,
|
.member-presence-status
|
||||||
.assignee-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-member
|
||||||
&.add-assignee
|
|
||||||
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,
|
.member
|
||||||
.assignee
|
|
||||||
width: 50px
|
width: 50px
|
||||||
height: @width
|
height: @width
|
||||||
margin-right: 10px
|
margin-right: 10px
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue