瀏覽代碼

My Cards and Due Cards development

* add spinner while pages are loading
* use a single publication for My Cards
* add Due Cards to the user menu
* add description to the All Users option for Due Cards
* some code clean-up
John R. Supplee 4 年之前
父節點
當前提交
ecc3558987

+ 31 - 27
client/components/main/dueCards.jade

@@ -1,7 +1,6 @@
 template(name="dueCardsHeaderBar")
   h1
-    //a.back-btn(href="{{pathFor 'home'}}")
-    //  i.fa.fa-chevron-left
+    i.fa.fa-calendar
     | {{_ 'dueCards-title'}}
 
   .board-header-btns.left
@@ -20,31 +19,33 @@ template(name="dueCardsModalTitle")
     | {{_ 'dueCards-title'}}
 
 template(name="dueCards")
-  .wrapper
-    .due-cards-dueat-list-wrapper
-      each card in dueCardsList
-        .due-cards-card-wrapper
-          a.minicard-wrapper.card-title(href=card.absoluteUrl)
-            +minicard(card)
-          ul.due-cards-context-list
-            li.due-cards-context(title="{{_ 'board'}}")
-              +viewer
-                = card.getBoard.title
-            li.due-cards-context.due-cards-context-separator
-              = ' '
-              | {{_ 'context-separator'}}
-              = ' '
-            li.due-cards-context(title="{{_ 'swimlane'}}")
-              +viewer
-                = card.getSwimlane.title
-            li.due-cards-context
-              = ' '
-              | {{_ 'context-separator'}}
-              = ' '
-            li.due-cards-context(title="{{_ 'list'}}")
-              +viewer
-                = card.getList.title
-
+  if isPageReady.get
+    .wrapper
+      .due-cards-dueat-list-wrapper
+        each card in dueCardsList
+          .due-cards-card-wrapper
+            a.minicard-wrapper.card-title(href=card.absoluteUrl)
+              +minicard(card)
+            ul.due-cards-context-list
+              li.due-cards-context(title="{{_ 'board'}}")
+                +viewer
+                  = card.getBoard.title
+              li.due-cards-context.due-cards-context-separator
+                = ' '
+                | {{_ 'context-separator'}}
+                = ' '
+              li.due-cards-context(title="{{_ 'swimlane'}}")
+                +viewer
+                  = card.getSwimlane.title
+              li.due-cards-context
+                = ' '
+                | {{_ 'context-separator'}}
+                = ' '
+              li.due-cards-context(title="{{_ 'list'}}")
+                +viewer
+                  = card.getList.title
+  else
+    +spinner
 
 template(name="dueCardsViewChangePopup")
   ul.pop-over-list
@@ -60,5 +61,8 @@ template(name="dueCardsViewChangePopup")
         a.js-due-cards-view-all
           i.fa.fa-users.colorful
           | {{_ 'dueCardsViewChange-choice-all'}}
+          span.sub-name
+            +viewer
+              | {{_ 'dueCardsViewChange-choice-all-description' }}
           if $eq Utils.dueCardsView "all"
             i.fa.fa-check

+ 15 - 1
client/components/main/dueCards.js

@@ -1,3 +1,5 @@
+const subManager = new SubsManager();
+
 BlazeComponent.extendComponent({
   dueCardsView() {
     // eslint-disable-next-line no-console
@@ -40,8 +42,20 @@ BlazeComponent.extendComponent({
 
 BlazeComponent.extendComponent({
   onCreated() {
+    this.isPageReady = new ReactiveVar(false);
+
+    this.autorun(() => {
+      const handle = subManager.subscribe(
+        'dueCards',
+        Utils.dueCardsView() === 'all',
+      );
+      Tracker.nonreactive(() => {
+        Tracker.autorun(() => {
+          this.isPageReady.set(handle.ready());
+        });
+      });
+    });
     Meteor.subscribe('setting');
-    Meteor.subscribe('dueCards', Utils.dueCardsView() === 'all');
   },
 
   dueCardsView() {

+ 46 - 43
client/components/main/myCards.jade

@@ -2,6 +2,7 @@ template(name="myCardsHeaderBar")
   h1
     //a.back-btn(href="{{pathFor 'home'}}")
     //  i.fa.fa-chevron-left
+    i.fa.fa-list
     | {{_ 'my-cards'}}
 
   .board-header-btns.left
@@ -20,51 +21,53 @@ template(name="myCardsModalTitle")
     | {{_ 'my-cards'}}
 
 template(name="myCards")
-  .wrapper
-    if $eq myCardsSort 'board'
-      each board in myBoards
-        .my-cards-board-wrapper
-          .my-cards-board-title
-            +viewer
-              = board.title
-          each swimlane in board.mySwimlanes
-            .my-cards-swimlane-title(class="{{#if swimlane.colorClass}}{{ swimlane.colorClass }}{{else}}swimlane-default-color{{/if}}")
+  if isPageReady.get
+    .wrapper
+      if $eq myCardsSort 'board'
+        each board in myCardsList
+          .my-cards-board-wrapper
+            .my-cards-board-title
               +viewer
-                = swimlane.title
-            each list in swimlane.myLists
-              .my-cards-list-wrapper
-                .my-cards-list-title(class=list.colorClass)
-                  +viewer
-                    = list.title
-                each card in list.myCards
-                  .my-cards-card-wrapper
-                    a.minicard-wrapper(href=card.absoluteUrl)
-                      +minicard(card)
-    else
-      .my-cards-dueat-list-wrapper
-        each card in myCardsList
-          .my-cards-card-wrapper
-            a.minicard-wrapper.card-title(href=card.absoluteUrl)
-              +minicard(card)
-            ul.my-cards-context-list
-              li.my-cards-context(title="{{_ 'board'}}")
-                +viewer
-                  = card.getBoard.title
-              li.my-cards-context.my-cards-context-separator
-                = ' '
-                | {{_ 'context-separator'}}
-                = ' '
-              li.my-cards-context(title="{{_ 'swimlane'}}")
-                +viewer
-                  = card.getSwimlane.title
-              li.my-cards-context
-                = ' '
-                | {{_ 'context-separator'}}
-                = ' '
-              li.my-cards-context(title="{{_ 'list'}}")
+                = board.title
+            each swimlane in board.mySwimlanes
+              .my-cards-swimlane-title(class="{{#if swimlane.colorClass}}{{ swimlane.colorClass }}{{else}}swimlane-default-color{{/if}}")
                 +viewer
-                  = card.getList.title
-
+                  = swimlane.title
+              each list in swimlane.myLists
+                .my-cards-list-wrapper
+                  .my-cards-list-title(class=list.colorClass)
+                    +viewer
+                      = list.title
+                  each card in list.myCards
+                    .my-cards-card-wrapper
+                      a.minicard-wrapper(href=card.absoluteUrl)
+                        +minicard(card)
+      else
+        .my-cards-dueat-list-wrapper
+          each card in myDueCardsList
+            .my-cards-card-wrapper
+              a.minicard-wrapper.card-title(href=card.absoluteUrl)
+                +minicard(card)
+              ul.my-cards-context-list
+                li.my-cards-context(title="{{_ 'board'}}")
+                  +viewer
+                    = card.getBoard.title
+                li.my-cards-context.my-cards-context-separator
+                  = ' '
+                  | {{_ 'context-separator'}}
+                  = ' '
+                li.my-cards-context(title="{{_ 'swimlane'}}")
+                  +viewer
+                    = card.getSwimlane.title
+                li.my-cards-context
+                  = ' '
+                  | {{_ 'context-separator'}}
+                  = ' '
+                li.my-cards-context(title="{{_ 'list'}}")
+                  +viewer
+                    = card.getList.title
+  else
+    +spinner
 
 template(name="myCardsSortChangePopup")
   ul.pop-over-list

+ 14 - 5
client/components/main/myCards.js

@@ -1,3 +1,5 @@
+const subManager = new SubsManager();
+
 BlazeComponent.extendComponent({
   myCardsSort() {
     // eslint-disable-next-line no-console
@@ -42,10 +44,17 @@ BlazeComponent.extendComponent({
 
 BlazeComponent.extendComponent({
   onCreated() {
+    this.isPageReady = new ReactiveVar(false);
+
+    this.autorun(() => {
+      const handle = subManager.subscribe('myCards');
+      Tracker.nonreactive(() => {
+        Tracker.autorun(() => {
+          this.isPageReady.set(handle.ready());
+        });
+      });
+    });
     Meteor.subscribe('setting');
-    Meteor.subscribe('myCards');
-    Meteor.subscribe('mySwimlanes');
-    Meteor.subscribe('myLists');
   },
 
   myCardsSort() {
@@ -58,7 +67,7 @@ BlazeComponent.extendComponent({
     return this.myCardsSort() === 'board';
   },
 
-  myBoards() {
+  myCardsList() {
     const userId = Meteor.userId();
     const boards = [];
     let board = null;
@@ -173,7 +182,7 @@ BlazeComponent.extendComponent({
     return boards;
   },
 
-  myCardsList() {
+  myDueCardsList() {
     const userId = Meteor.userId();
 
     const cursor = Cards.find(

+ 4 - 0
client/components/users/userHeader.jade

@@ -17,6 +17,10 @@ template(name="memberMenuPopup")
         a.js-my-cards(href="{{pathFor 'my-cards'}}")
           i.fa.fa-list
           | {{_ 'my-cards'}}
+      li
+        a.js-due-cards(href="{{pathFor 'due-cards'}}")
+          i.fa.fa-calendar
+          | {{_ 'dueCards-title'}}
       li
         a(href="{{pathFor 'home'}}")
           span.fa.fa-home

+ 3 - 0
client/components/users/userHeader.js

@@ -28,6 +28,9 @@ Template.memberMenuPopup.events({
   'click .js-my-cards'() {
     Popup.close();
   },
+  'click .js-due-cards'() {
+    Popup.close();
+  },
   'click .js-open-archived-board'() {
     Modal.open('archivedBoards');
   },

+ 2 - 18
config/router.js

@@ -116,8 +116,6 @@ FlowRouter.route('/shortcuts', {
 FlowRouter.route('/my-cards', {
   name: 'my-cards',
   action() {
-    const myCardsTemplate = 'myCards';
-
     Filter.reset();
     // EscapeActions.executeAll();
     EscapeActions.executeUpTo('popup-close');
@@ -125,15 +123,9 @@ FlowRouter.route('/my-cards', {
     Utils.manageCustomUI();
     Utils.manageMatomo();
 
-    // if (previousPath) {
-    //   Modal.open(myCardsTemplate, {
-    //     header: 'myCardsModalTitle',
-    //     onCloseGoTo: previousPath,
-    //   });
-    // } else {
     BlazeLayout.render('defaultLayout', {
       headerBar: 'myCardsHeaderBar',
-      content: myCardsTemplate,
+      content: 'myCards',
     });
     // }
   },
@@ -142,8 +134,6 @@ FlowRouter.route('/my-cards', {
 FlowRouter.route('/due-cards', {
   name: 'due-cards',
   action() {
-    const dueCardsTemplate = 'dueCards';
-
     Filter.reset();
     // EscapeActions.executeAll();
     EscapeActions.executeUpTo('popup-close');
@@ -151,15 +141,9 @@ FlowRouter.route('/due-cards', {
     Utils.manageCustomUI();
     Utils.manageMatomo();
 
-    // if (previousPath) {
-    //   Modal.open(dueCardsTemplate, {
-    //     header: 'dueCardsModalTitle',
-    //     onCloseGoTo: previousPath,
-    //   });
-    // } else {
     BlazeLayout.render('defaultLayout', {
       headerBar: 'dueCardsHeaderBar',
-      content: dueCardsTemplate,
+      content: 'dueCards',
     });
     // }
   },

+ 2 - 1
i18n/en.i18n.json

@@ -860,5 +860,6 @@
   "dueCards-title": "Due Cards",
   "dueCardsViewChange-title": "Due Cards View",
   "dueCardsViewChange-choice-me": "Me",
-  "dueCardsViewChange-choice-all": "All Users"
+  "dueCardsViewChange-choice-all": "All Users",
+  "dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission."
 }

+ 0 - 65
server/publications/boards.js

@@ -43,71 +43,6 @@ Meteor.publish('boards', function() {
   );
 });
 
-Meteor.publish('mySwimlanes', function() {
-  const userId = this.userId;
-  const swimlanes = [];
-
-  Cards.find({
-    archived: false,
-    $or: [{ members: userId }, { assignees: userId }],
-  }).forEach(card => {
-    swimlanes.push(card.swimlaneId);
-  });
-
-  return Swimlanes.find(
-    {
-      // archived: false,
-      _id: { $in: swimlanes },
-    },
-    {
-      fields: {
-        _id: 1,
-        title: 1,
-        boardId: 1,
-        type: 1,
-        color: 1,
-        sort: 1,
-      },
-      // sort: {
-      //   sort: ['boardId', 'listId', 'sort'],
-      // },
-    },
-  );
-});
-
-Meteor.publish('myLists', function() {
-  const userId = this.userId;
-  const lists = [];
-
-  Cards.find({
-    archived: false,
-    $or: [{ members: userId }, { assignees: userId }],
-  }).forEach(card => {
-    lists.push(card.listId);
-  });
-
-  return Lists.find(
-    {
-      // archived: false,
-      _id: { $in: lists },
-    },
-    {
-      fields: {
-        _id: 1,
-        boardId: 1,
-        swimlaneId: 1,
-        title: 1,
-        color: 1,
-        type: 1,
-        sort: 1,
-      },
-      // sort: {
-      //   sort: ['boardId', 'listId', 'sort'],
-      // },
-    },
-  );
-});
-
 Meteor.publish('archivedBoards', function() {
   const userId = this.userId;
   if (!Match.test(userId, String)) return [];

+ 80 - 25
server/publications/cards.js

@@ -4,33 +4,76 @@ Meteor.publish('card', cardId => {
 });
 
 Meteor.publish('myCards', function() {
-  const userId = this.userId;
+  const userId = Meteor.userId();
 
-  return Cards.find(
-    {
-      archived: false,
-      $or: [{ members: userId }, { assignees: userId }],
-    },
-    {
-      fields: {
-        _id: 1,
-        archived: 1,
-        boardId: 1,
-        swimlaneId: 1,
-        listId: 1,
-        title: 1,
-        type: 1,
-        sort: 1,
-        members: 1,
-        assignees: 1,
-        colors: 1,
-        dueAt: 1,
-      },
-      // sort: {
-      //   sort: ['boardId', 'listId', 'sort'],
-      // },
+  const archivedBoards = [];
+  Boards.find({ archived: true }).forEach(board => {
+    archivedBoards.push(board._id);
+  });
+
+  const archivedSwimlanes = [];
+  Swimlanes.find({ archived: true }).forEach(swimlane => {
+    archivedSwimlanes.push(swimlane._id);
+  });
+
+  const archivedLists = [];
+  Lists.find({ archived: true }).forEach(list => {
+    archivedLists.push(list._id);
+  });
+
+  selector = {
+    archived: false,
+    boardId: { $nin: archivedBoards },
+    swimlaneId: { $nin: archivedSwimlanes },
+    listId: { $nin: archivedLists },
+    $or: [{ members: userId }, { assignees: userId }],
+  };
+
+  const cards = Cards.find(selector, {
+    fields: {
+      _id: 1,
+      archived: 1,
+      boardId: 1,
+      swimlaneId: 1,
+      listId: 1,
+      title: 1,
+      type: 1,
+      sort: 1,
+      members: 1,
+      assignees: 1,
+      colors: 1,
+      dueAt: 1,
     },
-  );
+  });
+
+  const boards = [];
+  const swimlanes = [];
+  const lists = [];
+  const users = [];
+
+  cards.forEach(card => {
+    if (card.boardId) boards.push(card.boardId);
+    if (card.swimlaneId) swimlanes.push(card.swimlaneId);
+    if (card.listId) lists.push(card.listId);
+    if (card.members) {
+      card.members.forEach(userId => {
+        users.push(userId);
+      });
+    }
+    if (card.assignees) {
+      card.assignees.forEach(userId => {
+        users.push(userId);
+      });
+    }
+  });
+
+  return [
+    cards,
+    Boards.find({ _id: { $in: boards } }),
+    Swimlanes.find({ _id: { $in: swimlanes } }),
+    Lists.find({ _id: { $in: lists } }),
+    Users.find({ _id: { $in: users } }),
+  ];
 });
 
 Meteor.publish('dueCards', function(allUsers = false) {
@@ -105,11 +148,22 @@ Meteor.publish('dueCards', function(allUsers = false) {
   const boards = [];
   const swimlanes = [];
   const lists = [];
+  const users = [];
 
   cards.forEach(card => {
     if (card.boardId) boards.push(card.boardId);
     if (card.swimlaneId) swimlanes.push(card.swimlaneId);
     if (card.listId) lists.push(card.listId);
+    if (card.members) {
+      card.members.forEach(userId => {
+        users.push(userId);
+      });
+    }
+    if (card.assignees) {
+      card.assignees.forEach(userId => {
+        users.push(userId);
+      });
+    }
   });
 
   return [
@@ -117,5 +171,6 @@ Meteor.publish('dueCards', function(allUsers = false) {
     Boards.find({ _id: { $in: boards } }),
     Swimlanes.find({ _id: { $in: swimlanes } }),
     Lists.find({ _id: { $in: lists } }),
+    Users.find({ _id: { $in: users } }),
   ];
 });