summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/components/cards/cardDetails.jade37
-rw-r--r--client/components/cards/cardDetails.js77
-rw-r--r--client/components/cards/cardDetails.styl8
-rw-r--r--client/components/cards/minicard.jade4
-rw-r--r--i18n/en.i18n.json6
-rw-r--r--models/cards.js121
6 files changed, 237 insertions, 16 deletions
diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade
index 257ca0a8..9cd581ea 100644
--- a/client/components/cards/cardDetails.jade
+++ b/client/components/cards/cardDetails.jade
@@ -199,6 +199,24 @@ template(name="cardDetails")
+viewer
= getAssignedBy
+ if getVoteQuestion
+ hr
+ .vote-title
+ h3
+ i.fa.fa-thumbs-up
+ card-details-item-title {{_ 'vote-question'}}
+ .vote-result
+ .card-label.card-label-green
+ +viewer
+ = voteCountPositive
+ .card-label.card-label-red
+ +viewer
+ = voteCountNegative
+ +viewer
+ = getVoteQuestion
+ button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}") {{_ 'vote-for-it'}}
+ button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'vote-against'}}
+
//- XXX We should use "editable" to avoid repetiting ourselves
if canModifyCard
unless currentUser.isWorker
@@ -315,6 +333,16 @@ template(name="cardDetailsActionsPopup")
//li: a.js-members {{_ 'card-edit-members'}}
//li: a.js-labels {{_ 'card-edit-labels'}}
//li: a.js-attachments {{_ 'card-edit-attachments'}}
+ if getVoteQuestion
+ li
+ a.js-cancel-voting
+ i.fa.fa-thumbs-up
+ | {{_ 'card-cancel-voting'}}
+ else
+ li
+ a.js-start-voting
+ i.fa.fa-thumbs-up
+ | {{_ 'card-start-voting'}}
li
a.js-custom-fields
i.fa.fa-list-alt
@@ -538,3 +566,12 @@ template(name="cardDeletePopup")
unless archived
p {{_ "card-delete-suggest-archive"}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="cardStartVotingPopup")
+ form.edit-vote-question
+ .fields
+ label(for="vote") {{_ 'vote-question'}}
+ input.js-vote-field#vote(type="text" name="vote" value="{{card.getVoteQuestion}}" autofocus)
+
+ button.primary.confirm.js-submit {{_ 'save'}}
+ //- button.js-remove-color.negate.wide.right {{_ 'delete'}}
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index 5fdc5579..8492393c 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -38,6 +38,34 @@ BlazeComponent.extendComponent({
Meteor.subscribe('unsaved-edits');
},
+ voteState() {
+ const card = this.currentData();
+ const userId = Meteor.userId()
+ let state
+ if (card.vote) {
+ if (card.vote.positive) {
+ state = _.contains(card.vote.positive, userId);
+ if (state === true) return true
+ }
+ if (card.vote.negative) {
+ state = _.contains(card.vote.negative, userId);
+ if (state === true) return false
+ }
+ }
+ return null
+ },
+ voteCountPositive() {
+ const card = this.currentData();
+ if (card.vote && card.vote.positive)
+ return card.vote.positive.length
+ return null
+ },
+ voteCountNegative() {
+ const card = this.currentData();
+ if (card.vote && card.vote.negative)
+ return card.vote.negative.length
+ return null
+ },
isWatching() {
const card = this.currentData();
return card.findWatcher(Meteor.userId());
@@ -379,6 +407,18 @@ BlazeComponent.extendComponent({
'click #toggleButton'() {
Meteor.call('toggleSystemMessages');
},
+ 'click .js-vote'(e) {
+ const forIt = $(e.target).hasClass('js-vote-positive')
+ let newState = null
+ if (
+ this.voteState() == null ||
+ this.voteState() == false && forIt ||
+ this.voteState() == true && !forIt
+ ) {
+ newState = forIt
+ }
+ this.data().setVote(Meteor.userId(), newState)
+ }
},
];
},
@@ -560,6 +600,7 @@ Template.cardDetailsActionsPopup.events({
'click .js-assignees': Popup.open('cardAssignees'),
'click .js-labels': Popup.open('cardLabels'),
'click .js-attachments': Popup.open('cardAttachments'),
+ 'click .js-start-voting': Popup.open('cardStartVoting'),
'click .js-custom-fields': Popup.open('cardCustomFields'),
'click .js-received-date': Popup.open('editCardReceivedDate'),
'click .js-start-date': Popup.open('editCardStartDate'),
@@ -570,6 +611,11 @@ Template.cardDetailsActionsPopup.events({
'click .js-copy-card': Popup.open('copyCard'),
'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
'click .js-set-card-color': Popup.open('setCardColor'),
+ 'click .js-cancel-voting'(event) {
+ event.preventDefault();
+ this.unsetVote()
+ Popup.close();
+ },
'click .js-move-card-to-top'(event) {
event.preventDefault();
const minOrder = _.min(
@@ -603,7 +649,7 @@ Template.cardDetailsActionsPopup.events({
},
});
-Template.editCardTitleForm.onRendered(function() {
+Template.editCardTitleForm.onRendered(function () {
autosize(this.$('.js-edit-card-title'));
});
@@ -617,7 +663,7 @@ Template.editCardTitleForm.events({
},
});
-Template.editCardRequesterForm.onRendered(function() {
+Template.editCardRequesterForm.onRendered(function () {
autosize(this.$('.js-edit-card-requester'));
});
@@ -630,7 +676,7 @@ Template.editCardRequesterForm.events({
},
});
-Template.editCardAssignerForm.onRendered(function() {
+Template.editCardAssignerForm.onRendered(function () {
autosize(this.$('.js-edit-card-assigner'));
});
@@ -770,7 +816,7 @@ Template.copyChecklistToManyCardsPopup.events({
// copy subtasks
cursor = Cards.find({ parentId: oldId });
- cursor.forEach(function() {
+ cursor.forEach(function () {
'use strict';
const subtask = arguments[0];
subtask.parentId = _id;
@@ -919,7 +965,7 @@ BlazeComponent.extendComponent({
}
}
},
- 'click .js-delete': Popup.afterConfirm('cardDelete', function() {
+ 'click .js-delete': Popup.afterConfirm('cardDelete', function () {
Popup.close();
Cards.remove(this._id);
Utils.goBoardId(this.boardId);
@@ -945,6 +991,27 @@ BlazeComponent.extendComponent({
},
}).register('cardMorePopup');
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentCard = this.currentData();
+ this.voteQuestion = new ReactiveVar(this.currentCard.voteQuestion);
+ },
+
+ events() {
+ return [
+ {
+ 'submit .edit-vote-question'(evt) {
+ evt.preventDefault();
+ const voteQuestion = evt.target.vote.value;
+ this.currentCard.setVoteQuestion(voteQuestion)
+ Popup.close();
+
+ },
+ },
+ ];
+ },
+}).register('cardStartVotingPopup');
+
// Close the card details pane by pressing escape
EscapeActions.register(
'detailsPane',
diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl
index 80fa87c0..9bbbf075 100644
--- a/client/components/cards/cardDetails.styl
+++ b/client/components/cards/cardDetails.styl
@@ -330,3 +330,11 @@ card-details-color(background, color...)
.card-details-indigo
card-details-color(#4b0082, #ffffff) //White text for better visibility
+
+.voted
+ opacity: .7
+.vote-title
+ display: flex;
+ justify-content: space-between;
+.vote-result
+ display: flex;
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index a895c0a3..0b881a54 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -100,6 +100,10 @@ template(name="minicard")
if getDescription
.badge.badge-state-image-only(title=getDescription)
span.badge-icon.fa.fa-align-left
+ if getVoteQuestion
+ .badge.badge-state-image-only(title=getVoteQuestion)
+ span.badge-icon.fa.fa-thumbs-up
+ span.badge-icon.fa.fa-thumbs-down
if attachments.count
.badge
span.badge-icon.fa.fa-paperclip
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index a4493473..1ffdcc6b 100644
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -152,6 +152,8 @@
"card-spent": "Spent Time",
"card-edit-attachments": "Edit attachments",
"card-edit-custom-fields": "Edit custom fields",
+ "card-start-voting": "Start voting",
+ "card-cancel-voting": "Delete voting",
"card-edit-labels": "Edit labels",
"card-edit-members": "Edit members",
"card-labels-title": "Change the labels for the card.",
@@ -161,6 +163,10 @@
"cardAttachmentsPopup-title": "Attach From",
"cardCustomField-datePopup-title": "Change date",
"cardCustomFieldsPopup-title": "Edit custom fields",
+ "cardStartVotingPopup-title": "Start a vote",
+ "vote-question": "Voting question",
+ "vote-for-it": "for it",
+ "vote-against": "against",
"cardDeletePopup-title": "Delete Card?",
"cardDetailsActionsPopup-title": "Card Actions",
"cardLabelsPopup-title": "Labels",
diff --git a/models/cards.js b/models/cards.js
index eed1b958..1ee4ba68 100644
--- a/models/cards.js
+++ b/models/cards.js
@@ -304,6 +304,38 @@ Cards.attachSchema(
optional: true,
defaultValue: '',
},
+ vote: {
+ /**
+ * vote object, see below
+ */
+ type: Object,
+ optional: true,
+ },
+ 'vote.question': {
+ type: String,
+ defaultValue: '',
+ },
+ 'vote.positive': {
+ /**
+ * list of members (user IDs)
+ */
+ type: [String],
+ optional: true,
+ defaultValue: [],
+ },
+ 'vote.negative': {
+ /**
+ * list of members (user IDs)
+ */
+ type: [String],
+ optional: true,
+ defaultValue: [],
+ },
+ 'vote.end': {
+ type: Date,
+ optional: true,
+ defaultValue: null
+ }
}),
);
@@ -696,7 +728,7 @@ Cards.helpers({
parentString(sep) {
return this.parentList()
- .map(function(elem) {
+ .map(function (elem) {
return elem.title;
})
.join(sep);
@@ -980,6 +1012,22 @@ Cards.helpers({
}
},
+ getVoteQuestion() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({ _id: this.linkedId });
+ if (card && card.vote) return card.vote.question;
+ else return null;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId });
+ if (board && board.vote) return board.vote.question;
+ else return null;
+ } else if (this.vote) {
+ return this.vote.question;
+ } else {
+ return null;
+ }
+ },
+
getId() {
if (this.isLinked()) {
return this.linkedId;
@@ -1396,6 +1444,57 @@ Cards.mutations({
},
};
},
+ setVoteQuestion(question) {
+ return {
+ $set: {
+ vote: {
+ question,
+ positive:[],
+ negative:[]
+ },
+ }
+ }
+ },
+ unsetVote() {
+ return {
+ $unset: {
+ vote: '',
+ },
+ };
+ },
+ setVote(userId, forIt) {
+ switch (forIt) {
+ case true:
+ // vote for it
+ return {
+ $pull:{
+ "vote.negative": userId
+ },
+ $addToSet: {
+ "vote.positive": userId
+ }
+ }
+ case false:
+ // vote against
+ return {
+ $pull:{
+ "vote.positive": userId
+ },
+ $addToSet: {
+ "vote.negative" : userId
+ }
+ }
+
+ default:
+ // Remove votes
+ return {
+ $pull:{
+ "vote.positive": userId,
+ "vote.negative" : userId
+ },
+ }
+ }
+ },
});
//FUNCTIONS FOR creation of Activities
@@ -1798,7 +1897,7 @@ if (Meteor.isServer) {
});
//New activity for card moves
- Cards.after.update(function(userId, doc, fieldNames) {
+ Cards.after.update(function (userId, doc, fieldNames) {
const oldListId = this.previous.listId;
const oldSwimlaneId = this.previous.swimlaneId;
const oldBoardId = this.previous.boardId;
@@ -1844,7 +1943,7 @@ if (Meteor.isServer) {
// change list modifiedAt, when user modified the key values in timingaction array, if it's endAt, put the modifiedAt of list back to one year ago for sorting purpose
const modifiedAt = new Date(
new Date(value).getTime() -
- (action === 'endAt' ? 365 * 24 * 3600 * 1e3 : 0),
+ (action === 'endAt' ? 365 * 24 * 3600 * 1e3 : 0),
); // set it as 1 year before
const boardId = list.boardId;
Lists.direct.update(
@@ -1898,7 +1997,7 @@ if (Meteor.isServer) {
JsonRoutes.add(
'GET',
'/api/boards/:boardId/swimlanes/:swimlaneId/cards',
- function(req, res) {
+ function (req, res) {
const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
@@ -1908,7 +2007,7 @@ if (Meteor.isServer) {
boardId: paramBoardId,
swimlaneId: paramSwimlaneId,
archived: false,
- }).map(function(doc) {
+ }).map(function (doc) {
return {
_id: doc._id,
title: doc.title,
@@ -1932,7 +2031,7 @@ if (Meteor.isServer) {
* title: string,
* description: string}]
*/
- JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards', function(
+ JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards', function (
req,
res,
) {
@@ -1945,7 +2044,7 @@ if (Meteor.isServer) {
boardId: paramBoardId,
listId: paramListId,
archived: false,
- }).map(function(doc) {
+ }).map(function (doc) {
return {
_id: doc._id,
title: doc.title,
@@ -1967,7 +2066,7 @@ if (Meteor.isServer) {
JsonRoutes.add(
'GET',
'/api/boards/:boardId/lists/:listId/cards/:cardId',
- function(req, res) {
+ function (req, res) {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
const paramCardId = req.params.cardId;
@@ -1999,7 +2098,7 @@ if (Meteor.isServer) {
* @param {string} [assignees] the array of maximum one ID of assignee of the new card
* @return_type {_id: string}
*/
- JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function(
+ JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function (
req,
res,
) {
@@ -2106,7 +2205,7 @@ if (Meteor.isServer) {
JsonRoutes.add(
'PUT',
'/api/boards/:boardId/lists/:listId/cards/:cardId',
- function(req, res) {
+ function (req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId;
@@ -2405,7 +2504,7 @@ if (Meteor.isServer) {
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/lists/:listId/cards/:cardId',
- function(req, res) {
+ function (req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;