فهرست منبع

Merge branch 'master' of https://github.com/NicoP-S/wekan into NicoP-S-master

Lauri Ojansivu 5 سال پیش
والد
کامیت
9c97c301bd

+ 37 - 0
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'}}

+ 72 - 5
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',

+ 8 - 0
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;

+ 4 - 0
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

+ 6 - 0
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",

+ 110 - 11
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;