Explorar el Código

Card vote options in new fork

Nico hace 5 años
padre
commit
3cc0a93e0e

+ 1 - 2
client/components/boards/boardsList.js

@@ -25,7 +25,6 @@ BlazeComponent.extendComponent({
   },
 
   onRendered() {
-    const self = this;
     function userIsAllowedToMove() {
       return Meteor.user();
     }
@@ -78,7 +77,7 @@ BlazeComponent.extendComponent({
   },
 
   boards() {
-    let query = {
+    const query = {
       archived: false,
       type: 'board',
     };

+ 27 - 0
client/components/cards/cardDate.js

@@ -386,3 +386,30 @@ CardEndDate.register('cardEndDate');
     return this.date.get().format('l');
   }
 }.register('minicardEndDate'));
+
+class VoteEndDate extends CardDate {
+  onCreated() {
+    super.onCreated();
+    const self = this;
+    self.autorun(() => {
+      self.date.set(moment(self.data().getVoteEnd()));
+    });
+  }
+  classes() {
+    const classes = 'end-date' + ' ';
+    return classes;
+  }
+  showDate() {
+    return this.date.get().format('l LT');
+  }
+  showTitle() {
+    return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
+  }
+
+  events() {
+    return super.events().concat({
+      'click .js-edit-date': Popup.open('editVoteEndDate'),
+    });
+  }
+}
+VoteEndDate.register('voteEndDate');

+ 49 - 30
client/components/cards/cardDetails.jade

@@ -202,9 +202,12 @@ template(name="cardDetails")
     if getVoteQuestion
       hr
       .vote-title
-        h3
-          i.fa.fa-thumbs-up
-          card-details-item-title {{_ 'vote-question'}}
+        div.flex
+          h3
+            i.fa.fa-thumbs-up
+            | {{_ 'vote-question'}}
+          if getVoteEnd
+            +voteEndDate
         .vote-result
           if votePublic
             a.card-label.card-label-green.js-show-positive-votes {{ voteCountPositive }}
@@ -212,10 +215,13 @@ template(name="cardDetails")
           else
             .card-label.card-label-green {{ voteCountPositive }}
             .card-label.card-label-red {{ voteCountNegative }}
+          unless ($and currentBoard.isPublic voteAllowNonBoardMembers )
+            .card-label.card-label-gray  {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
       +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'}}
+      if showVotingButtons
+        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
@@ -333,16 +339,10 @@ 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-start-voting
+            i.fa.fa-thumbs-up
+            | {{_ 'card-edit-voting'}}
         li
           a.js-custom-fields
             i.fa.fa-list-alt
@@ -465,14 +465,14 @@ template(name="cardAssigneesPopup")
               i.fa.fa-check
   if currentUser.isWorker
     ul.pop-over-list.js-card-assignee-list
-        li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
-          a.name.js-select-assignee(href="#")
-            +userAvatar(userId=currentUser._id)
-            span.full-name
-              = currentUser.profile.fullname
-              | (<span class="username">{{ currentUser.username }}</span>)
-            if currentUser.isCardAssignee
-              i.fa.fa-check
+      li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
+        a.name.js-select-assignee(href="#")
+          +userAvatar(userId=currentUser._id)
+          span.full-name
+            = currentUser.profile.fullname
+            | (<span class="username">{{ currentUser.username }}</span>)
+          if currentUser.isCardAssignee
+            i.fa.fa-check
 
 template(name="userAvatarAssignee")
   a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})")
@@ -564,20 +564,39 @@ template(name="setCardColorPopup")
 template(name="cardDeletePopup")
   p {{_ "card-delete-pop"}}
   unless archived
-   p {{_ "card-delete-suggest-archive"}}
+    p {{_ "card-delete-suggest-archive"}}
+  button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="deleteVotePopup")
+  p {{_ "vote-delete-pop"}}
   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)
-      label(for="vote-public") {{_ 'vote-public'}}
-        a.js-toggle-vote-public
-          .materialCheckBox#vote-public(name="vote-public")
+      input.js-vote-field#vote(type="text" name="vote" value="{{getVoteQuestion}}" autofocus disabled="{{#if getVoteQuestion}}disabled{{/if}}")
+      .check-div
+        a.flex(class="{{#if getVoteQuestion}}is-disabled{{else}}js-toggle-vote-allow-non-members{{/if}}")
+          .materialCheckBox#vote-allow-non-members(name="vote-allow-non-members" class="{{#if voteAllowNonBoardMembers}}is-checked{{/if}}")
+          span {{_ 'allowNonBoardMembers'}}
+      .check-div
+        a.flex(class="{{#if getVoteQuestion}}is-disabled{{else}}js-toggle-vote-public{{/if}}")
+          .materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
+          span {{_ 'vote-public'}}
+      .check-div.flex
+        i.fa.fa-hourglass-end
+        a.js-end-date
+          span
+            | {{_ 'card-end'}}
+            unless getVoteEnd
+              i.fa.fa-plus
+        if getVoteEnd
+          +voteEndDate
 
-    button.primary.confirm.js-submit {{_ 'save'}}
-    //- button.js-remove-color.negate.wide.right {{_ 'delete'}}
+    button.primary.js-submit {{_ 'save'}}
+    if getVoteQuestion
+      button.js-remove-vote.negate.wide.right {{_ 'delete'}}
 
 template(name="positiveVoteMembersPopup")
   ul.pop-over-list.js-card-member-list

+ 81 - 21
client/components/cards/cardDetails.js

@@ -54,21 +54,6 @@ BlazeComponent.extendComponent({
     }
     return null;
   },
-  votePublic() {
-    const card = this.currentData();
-    if (card.vote) return card.vote.public;
-    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());
@@ -148,6 +133,15 @@ BlazeComponent.extendComponent({
     return result;
   },
 
+  showVotingButtons() {
+    const card = this.currentData();
+    return (
+      (currentUser.isBoardMember() ||
+        (currentUser && card.voteAllowNonBoardMembers())) &&
+      !card.expiredVote()
+    );
+  },
+
   onRendered() {
     if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
       // Send Webhook but not create Activities records ---
@@ -611,11 +605,6 @@ 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(
@@ -1000,22 +989,93 @@ BlazeComponent.extendComponent({
   events() {
     return [
       {
+        'click .js-end-date': Popup.open('editVoteEndDate'),
         'submit .edit-vote-question'(evt) {
           evt.preventDefault();
           const voteQuestion = evt.target.vote.value;
           const publicVote = $('#vote-public').hasClass('is-checked');
-          this.currentCard.setVoteQuestion(voteQuestion, publicVote);
+          const allowNonBoardMembers = $('#vote-allow-non-members').hasClass(
+            'is-checked',
+          );
+          const endString = this.currentCard.getVoteEnd();
+
+          this.currentCard.setVoteQuestion(
+            voteQuestion,
+            publicVote,
+            allowNonBoardMembers,
+          );
+          if (endString) {
+            this.currentCard.setVoteEnd(endString);
+          }
           Popup.close();
         },
+        'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
+          event.preventDefault();
+          this.currentCard.unsetVote();
+          Popup.close();
+        }),
         'click a.js-toggle-vote-public'(event) {
           event.preventDefault();
           $('#vote-public').toggleClass('is-checked');
         },
+        'click a.js-toggle-vote-allow-non-members'(event) {
+          event.preventDefault();
+          $('#vote-allow-non-members').toggleClass('is-checked');
+        },
       },
     ];
   },
 }).register('cardStartVotingPopup');
 
+// editVoteEndDatePopup
+(class extends DatePicker {
+  onCreated() {
+    super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
+    this.data().getVoteEnd() && this.date.set(moment(this.data().getVoteEnd()));
+  }
+  events() {
+    return [
+      {
+        'submit .edit-date'(evt) {
+          evt.preventDefault();
+
+          // if no time was given, init with 12:00
+          const time =
+            evt.target.time.value ||
+            moment(new Date().setHours(12, 0, 0)).format('LT');
+
+          const dateString = `${evt.target.date.value} ${time}`;
+          const newDate = moment(dateString, 'L LT', true);
+          if (newDate.isValid()) {
+            // if active vote -  store it
+            if (this.currentData().getVoteQuestion()) {
+              this._storeDate(newDate.toDate());
+              Popup.close();
+            } else {
+              this.currentData().vote = { end: newDate.toDate() }; // set vote end temp
+              Popup.back();
+            }
+          } else {
+            this.error.set('invalid-date');
+            evt.target.date.focus();
+          }
+        },
+        'click .js-delete-date'(evt) {
+          evt.preventDefault();
+          this._deleteDate();
+          Popup.close();
+        },
+      },
+    ];
+  }
+  _storeDate(newDate) {
+    this.card.setVoteEnd(newDate);
+  }
+  _deleteDate() {
+    this.card.unsetVoteEnd();
+  }
+}.register('editVoteEndDatePopup'));
+
 // Close the card details pane by pressing escape
 EscapeActions.register(
   'detailsPane',

+ 5 - 0
client/components/cards/cardDetails.styl

@@ -337,6 +337,11 @@ card-details-color(background, color...)
 .vote-title
   display: flex
   justify-content: space-between
+
+  .js-edit-date
+    align-self: baseline
+    margin-left: 5px
+
 .vote-result
   display: flex
 .js-show-positive-votes

+ 2 - 0
client/components/cards/minicard.jade

@@ -103,7 +103,9 @@ template(name="minicard")
       if getVoteQuestion
         .badge.badge-state-image-only(title=getVoteQuestion)
           span.badge-icon.fa.fa-thumbs-up
+          span.badge-text {{ voteCountPositive }}
           span.badge-icon.fa.fa-thumbs-down
+          span.badge-text {{ voteCountNegative }}
       if attachments.count
         .badge
           span.badge-icon.fa.fa-paperclip

+ 1 - 1
client/components/main/editor.js

@@ -330,7 +330,7 @@ Template.viewer.events({
   // the corresponding text). Clicking a link shouldn't fire these actions, stop
   // we stop these event at the viewer component level.
   'click a'(event, templateInstance) {
-    let prevent = true;
+    const prevent = true;
     const userId = event.currentTarget.dataset.userid;
     if (userId) {
       Popup.open('member').call({ userId }, event, templateInstance);

+ 5 - 7
i18n/en.i18n.json

@@ -152,8 +152,6 @@
   "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 and all votes",
   "card-edit-labels": "Edit labels",
   "card-edit-members": "Edit members",
   "card-labels-title": "Change the labels for the card.",
@@ -166,11 +164,15 @@
   "cardStartVotingPopup-title": "Start a vote",
   "positiveVoteMembersPopup-title": "Proponents",
   "negativeVoteMembersPopup-title": "Opponents",
-  "allowNonBoardMembers": "Allow anonymous vote on public board",
+  "card-edit-voting": "Edit voting",
+  "editVoteEndDatePopup-title": "Change vote end date",
+  "allowNonBoardMembers": "Allow all logged in users",
   "vote-question": "Voting question",
   "vote-public": "Show who voted what",
   "vote-for-it": "for it",
   "vote-against": "against",
+  "deleteVotePopup-title": "Delete vote?",
+  "vote-delete-pop": "Deleting is permanent. You will lose all actions associated with this vote.",
   "cardDeletePopup-title": "Delete Card?",
   "cardDetailsActionsPopup-title": "Card Actions",
   "cardLabelsPopup-title": "Labels",
@@ -642,8 +644,6 @@
   "r-when-a-member": "When a member is",
   "r-when-the-member": "When the member",
   "r-name": "name",
-  "r-is": "is",
-  "r-when-a-attach": "When an attachment",
   "r-when-a-checklist": "When a checklist is",
   "r-when-the-checklist": "When the checklist",
   "r-completed": "Completed",
@@ -656,7 +656,6 @@
   "r-top-of": "Top of",
   "r-bottom-of": "Bottom of",
   "r-its-list": "its list",
-  "r-list": "list",
   "r-archive": "Move to Archive",
   "r-unarchive": "Restore from Archive",
   "r-card": "card",
@@ -712,7 +711,6 @@
   "r-swimlane-name": "swimlane name",
   "r-board-note": "Note: leave a field empty to match every possible value. ",
   "r-checklist-note": "Note: checklist's items have to be written as comma separated values.",
-  "r-added-to": "added to",
   "r-when-a-card-is-moved": "When a card is moved to another list",
   "r-set": "Set",
   "r-update": "Update",

+ 67 - 3
models/cards.js

@@ -340,6 +340,10 @@ Cards.attachSchema(
       type: Boolean,
       defaultValue: false,
     },
+    'vote.allowNonBoardMembers': {
+      type: Boolean,
+      defaultValue: false,
+    },
   }),
 );
 
@@ -347,8 +351,14 @@ Cards.allow({
   insert(userId, doc) {
     return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
   },
-  update(userId, doc) {
-    return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+
+  update(userId, doc, fields) {
+    // Allow board members or logged in users if only vote get's changed
+    return (
+      allowIsBoardMember(userId, Boards.findOne(doc.boardId)) ||
+      (_.isEqual(fields, ['vote', 'modifiedAt', 'dateLastActivity']) &&
+        !!userId)
+    );
   },
   remove(userId, doc) {
     return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
@@ -1048,6 +1058,29 @@ Cards.helpers({
     }
   },
 
+  getVoteEnd() {
+    if (this.isLinkedCard()) {
+      const card = Cards.findOne({ _id: this.linkedId });
+      if (card && card.vote) return card.vote.end;
+      else return null;
+    } else if (this.isLinkedBoard()) {
+      const board = Boards.findOne({ _id: this.linkedId });
+      if (board && board.vote) return board.vote.end;
+      else return null;
+    } else if (this.vote) {
+      return this.vote.end;
+    } else {
+      return null;
+    }
+  },
+  expiredVote() {
+    let end = this.getVoteEnd();
+    if (end) {
+      end = moment(end);
+      return end.isBefore(new Date());
+    }
+    return false;
+  },
   voteMemberPositive() {
     if (this.vote && this.vote.positive)
       return Users.find({ _id: { $in: this.vote.positive } });
@@ -1153,6 +1186,26 @@ Cards.helpers({
   isTemplateCard() {
     return this.type === 'template-card';
   },
+
+  votePublic() {
+    if (this.vote) return this.vote.public;
+    return null;
+  },
+  voteAllowNonBoardMembers() {
+    if (this.vote) return this.vote.allowNonBoardMembers;
+    return null;
+  },
+  voteCountNegative() {
+    if (this.vote && this.vote.negative) return this.vote.negative.length;
+    return null;
+  },
+  voteCountPositive() {
+    if (this.vote && this.vote.positive) return this.vote.positive.length;
+    return null;
+  },
+  voteCount() {
+    return this.voteCountPositive() + this.voteCountNegative();
+  },
 });
 
 Cards.mutations({
@@ -1476,12 +1529,13 @@ Cards.mutations({
       },
     };
   },
-  setVoteQuestion(question, publicVote) {
+  setVoteQuestion(question, publicVote, allowNonBoardMembers) {
     return {
       $set: {
         vote: {
           question,
           public: publicVote,
+          allowNonBoardMembers,
           positive: [],
           negative: [],
         },
@@ -1495,6 +1549,16 @@ Cards.mutations({
       },
     };
   },
+  setVoteEnd(end) {
+    return {
+      $set: { 'vote.end': end },
+    };
+  },
+  unsetVoteEnd() {
+    return {
+      $unset: { 'vote.end': '' },
+    };
+  },
   setVote(userId, forIt) {
     switch (forIt) {
       case true:

+ 1 - 1
sandstorm.js

@@ -22,7 +22,7 @@ const sandstormBoard = {
 
 if (isSandstorm && Meteor.isServer) {
   const fs = require('fs');
-  const Capnp = Npm.require(`capnp`);
+  const Capnp = Npm.require('capnp');
   const Package = Capnp.importSystem('sandstorm/package.capnp');
   const Powerbox = Capnp.importSystem('sandstorm/powerbox.capnp');
   const Identity = Capnp.importSystem('sandstorm/identity.capnp');