From 2bbc312ad0600da06b7d18f57630ad19cd90efd2 Mon Sep 17 00:00:00 2001 From: Nico Date: Tue, 7 Apr 2020 20:43:35 +0200 Subject: Voteing feature --- client/components/cards/cardDetails.jade | 37 ++++++++++ client/components/cards/cardDetails.js | 77 ++++++++++++++++++-- client/components/cards/cardDetails.styl | 8 ++ client/components/cards/minicard.jade | 4 + i18n/en.i18n.json | 6 ++ models/cards.js | 121 ++++++++++++++++++++++++++++--- 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 e3b3288d..c37af44c 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; -- cgit v1.2.3-1-g7c22