Przeglądaj źródła

Start work on paging search results

John R. Supplee 4 lat temu
rodzic
commit
4e8fc46475

+ 6 - 0
client/components/main/globalSearch.jade

@@ -37,6 +37,12 @@ template(name="globalSearch")
               a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
             each card in results.get
               +resultCard(card)
+            if hasPreviousPage.get
+              button.js-previous-page
+                | {{_ 'previous-page' }}
+            if hasNextPage.get
+              button.js-next-page
+                | {{_ 'next-page' }}
       else
         .global-search-instructions
           h2 {{_ 'boards' }}

+ 84 - 7
client/components/main/globalSearch.js

@@ -46,12 +46,15 @@ BlazeComponent.extendComponent({
     this.myLabelNames = new ReactiveVar([]);
     this.myBoardNames = new ReactiveVar([]);
     this.results = new ReactiveVar([]);
+    this.hasNextPage = new ReactiveVar(false);
+    this.hasPreviousPage = new ReactiveVar(false);
     this.queryParams = null;
     this.parsingErrors = [];
     this.resultsCount = 0;
     this.totalHits = 0;
     this.queryErrors = null;
     this.colorMap = null;
+    this.resultsPerPage = 25;
 
     Meteor.call('myLists', (err, data) => {
       if (!err) {
@@ -100,17 +103,21 @@ BlazeComponent.extendComponent({
     this.queryErrors = null;
   },
 
+  getSessionData() {
+    return SessionData.findOne({
+      userId: Meteor.userId(),
+      sessionId: SessionData.getSessionId(),
+    });
+  },
+
   getResults() {
     // eslint-disable-next-line no-console
     // console.log('getting results');
     if (this.queryParams) {
-      const sessionData = SessionData.findOne({
-        userId: Meteor.userId(),
-        sessionId: SessionData.getSessionId(),
-      });
+      const sessionData = this.getSessionData();
       // eslint-disable-next-line no-console
+      console.log('selector:', JSON.parse(sessionData.selector));
       // console.log('session data:', sessionData);
-
       const cards = Cards.find({ _id: { $in: sessionData.cards } });
       this.queryErrors = sessionData.errors;
       if (this.queryErrors.length) {
@@ -121,8 +128,14 @@ BlazeComponent.extendComponent({
       if (cards) {
         this.totalHits = sessionData.totalHits;
         this.resultsCount = cards.count();
+        this.resultsStart = sessionData.lastHit - this.resultsCount + 1;
+        this.resultsEnd = sessionData.lastHit;
         this.resultsHeading.set(this.getResultsHeading());
         this.results.set(cards);
+        this.hasNextPage.set(sessionData.lastHit < sessionData.totalHits);
+        this.hasPreviousPage.set(
+          sessionData.lastHit - sessionData.resultsCount > 0,
+        );
       }
     }
     this.resultsCount = 0;
@@ -243,6 +256,7 @@ BlazeComponent.extendComponent({
     // console.log('operatorMap:', operatorMap);
 
     const params = {
+      limit: this.resultsPerPage,
       boards: [],
       swimlanes: [],
       lists: [],
@@ -395,6 +409,61 @@ BlazeComponent.extendComponent({
     });
   },
 
+  nextPage() {
+    sessionData = this.getSessionData();
+
+    const params = {
+      limit: this.resultsPerPage,
+      selector: JSON.parse(sessionData.selector),
+      skip: sessionData.lastHit,
+    };
+
+    this.autorun(() => {
+      const handle = Meteor.subscribe(
+        'globalSearch',
+        SessionData.getSessionId(),
+        params,
+      );
+      Tracker.nonreactive(() => {
+        Tracker.autorun(() => {
+          if (handle.ready()) {
+            this.getResults();
+            this.searching.set(false);
+            this.hasResults.set(true);
+          }
+        });
+      });
+    });
+  },
+
+  previousPage() {
+    sessionData = this.getSessionData();
+
+    const params = {
+      limit: this.resultsPerPage,
+      selector: JSON.parse(sessionData.selector),
+      skip:
+        sessionData.lastHit - sessionData.resultsCount - this.resultsPerPage,
+    };
+
+    this.autorun(() => {
+      const handle = Meteor.subscribe(
+        'globalSearch',
+        SessionData.getSessionId(),
+        params,
+      );
+      Tracker.nonreactive(() => {
+        Tracker.autorun(() => {
+          if (handle.ready()) {
+            this.getResults();
+            this.searching.set(false);
+            this.hasResults.set(true);
+          }
+        });
+      });
+    });
+  },
+
   getResultsHeading() {
     if (this.resultsCount === 0) {
       return TAPi18n.__('no-cards-found');
@@ -405,8 +474,8 @@ BlazeComponent.extendComponent({
     }
 
     return TAPi18n.__('n-n-of-n-cards-found', {
-      start: 1,
-      end: this.resultsCount,
+      start: this.resultsStart,
+      end: this.resultsEnd,
       total: this.totalHits,
     });
   },
@@ -526,6 +595,14 @@ BlazeComponent.extendComponent({
           evt.preventDefault();
           this.searchAllBoards(evt.target.searchQuery.value);
         },
+        'click .js-next-page'(evt) {
+          evt.preventDefault();
+          this.nextPage();
+        },
+        'click .js-previous-page'(evt) {
+          evt.preventDefault();
+          this.previousPage();
+        },
         'click .js-label-color'(evt) {
           evt.preventDefault();
           const input = document.getElementById('global-search-input');

+ 2 - 0
i18n/en.i18n.json

@@ -917,6 +917,8 @@
   "operator-number-expected": "operator __operator__ expected a number, got '__value__'",
   "operator-sort-invalid": "sort of '%s' is invalid",
   "operator-status-invalid": "'%s' is not a valid status",
+  "next-page": "Next Page",
+  "previous-page": "Previous Page",
   "heading-notes": "Notes",
   "globalSearch-instructions-heading": "Search Instructions",
   "globalSearch-instructions-description": "Searches can include operators to refine the search.  Operators are specified by writing the operator name and value separated by a colon.  For example, an operator specification of `list:Blocked` would limit the search to cards that are contained in a list named *Blocked*.  If the value contains spaces or special characters it must be enclosed in quotation marks (e.g. `__operator_list__:\"To Review\"`).",

+ 12 - 0
models/usersessiondata.js

@@ -39,6 +39,13 @@ SessionData.attachSchema(
       type: Number,
       optional: true,
     },
+    resultsCount: {
+      /**
+       * number of results returned
+       */
+      type: Number,
+      optional: true,
+    },
     lastHit: {
       /**
        * the last hit returned from a report query
@@ -50,6 +57,11 @@ SessionData.attachSchema(
       type: [String],
       optional: true,
     },
+    selector: {
+      type: String,
+      optional: true,
+      blackbox: true,
+    },
     errorMessages: {
       type: [String],
       optional: true,

+ 225 - 204
server/publications/cards.js

@@ -205,8 +205,8 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
     }
 
     hasErrors() {
-      for (const prop in this.notFound) {
-        if (this.notFound[prop].length) {
+      for (const value of Object.values(this.notFound)) {
+        if (value.length) {
           return true;
         }
       }
@@ -247,245 +247,255 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
     }
   })();
 
-  let archived = false;
-  let endAt = null;
-  if (queryParams.status.length) {
-    queryParams.status.forEach(status => {
-      if (status === 'archived') {
-        archived = true;
-      } else if (status === 'all') {
-        archived = null;
-      } else if (status === 'ended') {
-        endAt = { $nin: [null, ''] };
-      }
-    });
+  let selector = {};
+  let skip = 0;
+  if (queryParams.skip) {
+    skip = queryParams.skip;
+  }
+  let limit = 25;
+  if (queryParams.limit) {
+    limit = queryParams.limit;
   }
-  const selector = {
-    type: 'cardType-card',
-    // boardId: { $in: Boards.userBoardIds(userId) },
-    $and: [],
-  };
 
-  const boardsSelector = {};
-  if (archived !== null) {
-    boardsSelector.archived = archived;
-    if (archived) {
-      selector.boardId = { $in: Boards.userBoardIds(userId, null) };
-      selector.$and.push({
-        $or: [
-          { boardId: { $in: Boards.userBoardIds(userId, archived) } },
-          { swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } },
-          { listId: { $in: Lists.archivedListIds() } },
-          { archived: true },
-        ],
+  if (queryParams.selector) {
+    selector = queryParams.selector;
+  } else {
+    let archived = false;
+    let endAt = null;
+    if (queryParams.status.length) {
+      queryParams.status.forEach(status => {
+        if (status === 'archived') {
+          archived = true;
+        } else if (status === 'all') {
+          archived = null;
+        } else if (status === 'ended') {
+          endAt = { $nin: [null, ''] };
+        }
       });
-    } else {
-      selector.boardId = { $in: Boards.userBoardIds(userId, false) };
-      selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() };
-      selector.listId = { $nin: Lists.archivedListIds() };
-      selector.archived = false;
     }
-  } else {
-    selector.boardId = { $in: Boards.userBoardIds(userId, null) };
-  }
-  if (endAt !== null) {
-    selector.endAt = endAt;
-  }
+    selector = {
+      type: 'cardType-card',
+      // boardId: { $in: Boards.userBoardIds(userId) },
+      $and: [],
+    };
 
-  if (queryParams.boards.length) {
-    const queryBoards = [];
-    queryParams.boards.forEach(query => {
-      const boards = Boards.userSearch(userId, {
-        title: new RegExp(escapeForRegex(query), 'i'),
-      });
-      if (boards.count()) {
-        boards.forEach(board => {
-          queryBoards.push(board._id);
+    const boardsSelector = {};
+    if (archived !== null) {
+      boardsSelector.archived = archived;
+      if (archived) {
+        selector.boardId = { $in: Boards.userBoardIds(userId, null) };
+        selector.$and.push({
+          $or: [
+            { boardId: { $in: Boards.userBoardIds(userId, archived) } },
+            { swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } },
+            { listId: { $in: Lists.archivedListIds() } },
+            { archived: true },
+          ],
         });
       } else {
-        errors.notFound.boards.push(query);
+        selector.boardId = { $in: Boards.userBoardIds(userId, false) };
+        selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() };
+        selector.listId = { $nin: Lists.archivedListIds() };
+        selector.archived = false;
       }
-    });
-
-    selector.boardId.$in = queryBoards;
-  }
+    } else {
+      selector.boardId = { $in: Boards.userBoardIds(userId, null) };
+    }
+    if (endAt !== null) {
+      selector.endAt = endAt;
+    }
 
-  if (queryParams.swimlanes.length) {
-    const querySwimlanes = [];
-    queryParams.swimlanes.forEach(query => {
-      const swimlanes = Swimlanes.find({
-        title: new RegExp(escapeForRegex(query), 'i'),
-      });
-      if (swimlanes.count()) {
-        swimlanes.forEach(swim => {
-          querySwimlanes.push(swim._id);
+    if (queryParams.boards.length) {
+      const queryBoards = [];
+      queryParams.boards.forEach(query => {
+        const boards = Boards.userSearch(userId, {
+          title: new RegExp(escapeForRegex(query), 'i'),
         });
-      } else {
-        errors.notFound.swimlanes.push(query);
-      }
-    });
+        if (boards.count()) {
+          boards.forEach(board => {
+            queryBoards.push(board._id);
+          });
+        } else {
+          errors.notFound.boards.push(query);
+        }
+      });
 
-    selector.swimlaneId.$in = querySwimlanes;
-  }
+      selector.boardId.$in = queryBoards;
+    }
 
-  if (queryParams.lists.length) {
-    const queryLists = [];
-    queryParams.lists.forEach(query => {
-      const lists = Lists.find({
-        title: new RegExp(escapeForRegex(query), 'i'),
+    if (queryParams.swimlanes.length) {
+      const querySwimlanes = [];
+      queryParams.swimlanes.forEach(query => {
+        const swimlanes = Swimlanes.find({
+          title: new RegExp(escapeForRegex(query), 'i'),
+        });
+        if (swimlanes.count()) {
+          swimlanes.forEach(swim => {
+            querySwimlanes.push(swim._id);
+          });
+        } else {
+          errors.notFound.swimlanes.push(query);
+        }
       });
-      if (lists.count()) {
-        lists.forEach(list => {
-          queryLists.push(list._id);
+
+      selector.swimlaneId.$in = querySwimlanes;
+    }
+
+    if (queryParams.lists.length) {
+      const queryLists = [];
+      queryParams.lists.forEach(query => {
+        const lists = Lists.find({
+          title: new RegExp(escapeForRegex(query), 'i'),
         });
-      } else {
-        errors.notFound.lists.push(query);
-      }
-    });
+        if (lists.count()) {
+          lists.forEach(list => {
+            queryLists.push(list._id);
+          });
+        } else {
+          errors.notFound.lists.push(query);
+        }
+      });
 
-    selector.listId.$in = queryLists;
-  }
+      selector.listId.$in = queryLists;
+    }
 
-  if (queryParams.comments.length) {
-    const cardIds = CardComments.textSearch(userId, queryParams.comments).map(
-      com => {
-        return com.cardId;
-      },
-    );
-    if (cardIds.length) {
-      selector._id = { $in: cardIds };
-    } else {
-      errors.notFound.comments.push(queryParams.comments);
+    if (queryParams.comments.length) {
+      const cardIds = CardComments.textSearch(userId, queryParams.comments).map(
+        com => {
+          return com.cardId;
+        },
+      );
+      if (cardIds.length) {
+        selector._id = { $in: cardIds };
+      } else {
+        errors.notFound.comments.push(queryParams.comments);
+      }
     }
-  }
 
-  if (queryParams.dueAt !== null) {
-    selector.dueAt = { $lte: new Date(queryParams.dueAt) };
-  }
+    if (queryParams.dueAt !== null) {
+      selector.dueAt = { $lte: new Date(queryParams.dueAt) };
+    }
 
-  if (queryParams.createdAt !== null) {
-    selector.createdAt = { $gte: new Date(queryParams.createdAt) };
-  }
+    if (queryParams.createdAt !== null) {
+      selector.createdAt = { $gte: new Date(queryParams.createdAt) };
+    }
 
-  if (queryParams.modifiedAt !== null) {
-    selector.modifiedAt = { $gte: new Date(queryParams.modifiedAt) };
-  }
+    if (queryParams.modifiedAt !== null) {
+      selector.modifiedAt = { $gte: new Date(queryParams.modifiedAt) };
+    }
 
-  const queryMembers = [];
-  const queryAssignees = [];
-  if (queryParams.users.length) {
-    queryParams.users.forEach(query => {
-      const users = Users.find({
-        username: query,
-      });
-      if (users.count()) {
-        users.forEach(user => {
-          queryMembers.push(user._id);
-          queryAssignees.push(user._id);
+    const queryMembers = [];
+    const queryAssignees = [];
+    if (queryParams.users.length) {
+      queryParams.users.forEach(query => {
+        const users = Users.find({
+          username: query,
         });
-      } else {
-        errors.notFound.users.push(query);
-      }
-    });
-  }
-
-  if (queryParams.members.length) {
-    queryParams.members.forEach(query => {
-      const users = Users.find({
-        username: query,
+        if (users.count()) {
+          users.forEach(user => {
+            queryMembers.push(user._id);
+            queryAssignees.push(user._id);
+          });
+        } else {
+          errors.notFound.users.push(query);
+        }
       });
-      if (users.count()) {
-        users.forEach(user => {
-          queryMembers.push(user._id);
-        });
-      } else {
-        errors.notFound.members.push(query);
-      }
-    });
-  }
+    }
 
-  if (queryParams.assignees.length) {
-    queryParams.assignees.forEach(query => {
-      const users = Users.find({
-        username: query,
-      });
-      if (users.count()) {
-        users.forEach(user => {
-          queryAssignees.push(user._id);
+    if (queryParams.members.length) {
+      queryParams.members.forEach(query => {
+        const users = Users.find({
+          username: query,
         });
-      } else {
-        errors.notFound.assignees.push(query);
-      }
-    });
-  }
-
-  if (queryMembers.length && queryAssignees.length) {
-    selector.$and.push({
-      $or: [
-        { members: { $in: queryMembers } },
-        { assignees: { $in: queryAssignees } },
-      ],
-    });
-  } else if (queryMembers.length) {
-    selector.members = { $in: queryMembers };
-  } else if (queryAssignees.length) {
-    selector.assignees = { $in: queryAssignees };
-  }
+        if (users.count()) {
+          users.forEach(user => {
+            queryMembers.push(user._id);
+          });
+        } else {
+          errors.notFound.members.push(query);
+        }
+      });
+    }
 
-  if (queryParams.labels.length) {
-    queryParams.labels.forEach(label => {
-      const queryLabels = [];
+    if (queryParams.assignees.length) {
+      queryParams.assignees.forEach(query => {
+        const users = Users.find({
+          username: query,
+        });
+        if (users.count()) {
+          users.forEach(user => {
+            queryAssignees.push(user._id);
+          });
+        } else {
+          errors.notFound.assignees.push(query);
+        }
+      });
+    }
 
-      let boards = Boards.userSearch(userId, {
-        labels: { $elemMatch: { color: label.toLowerCase() } },
+    if (queryMembers.length && queryAssignees.length) {
+      selector.$and.push({
+        $or: [
+          { members: { $in: queryMembers } },
+          { assignees: { $in: queryAssignees } },
+        ],
       });
+    } else if (queryMembers.length) {
+      selector.members = { $in: queryMembers };
+    } else if (queryAssignees.length) {
+      selector.assignees = { $in: queryAssignees };
+    }
 
-      if (boards.count()) {
-        boards.forEach(board => {
-          // eslint-disable-next-line no-console
-          // console.log('board:', board);
-          // eslint-disable-next-line no-console
-          // console.log('board.labels:', board.labels);
-          board.labels
-            .filter(boardLabel => {
-              return boardLabel.color === label.toLowerCase();
-            })
-            .forEach(boardLabel => {
-              queryLabels.push(boardLabel._id);
-            });
-        });
-      } else {
-        // eslint-disable-next-line no-console
-        // console.log('label:', label);
-        const reLabel = new RegExp(escapeForRegex(label), 'i');
-        // eslint-disable-next-line no-console
-        // console.log('reLabel:', reLabel);
-        boards = Boards.userSearch(userId, {
-          labels: { $elemMatch: { name: reLabel } },
+    if (queryParams.labels.length) {
+      queryParams.labels.forEach(label => {
+        const queryLabels = [];
+
+        let boards = Boards.userSearch(userId, {
+          labels: { $elemMatch: { color: label.toLowerCase() } },
         });
 
         if (boards.count()) {
           boards.forEach(board => {
+            // eslint-disable-next-line no-console
+            // console.log('board:', board);
+            // eslint-disable-next-line no-console
+            // console.log('board.labels:', board.labels);
             board.labels
               .filter(boardLabel => {
-                return boardLabel.name.match(reLabel);
+                return boardLabel.color === label.toLowerCase();
               })
               .forEach(boardLabel => {
                 queryLabels.push(boardLabel._id);
               });
           });
         } else {
-          errors.notFound.labels.push(label);
-        }
-      }
+          // eslint-disable-next-line no-console
+          // console.log('label:', label);
+          const reLabel = new RegExp(escapeForRegex(label), 'i');
+          // eslint-disable-next-line no-console
+          // console.log('reLabel:', reLabel);
+          boards = Boards.userSearch(userId, {
+            labels: { $elemMatch: { name: reLabel } },
+          });
 
-      selector.labelIds = { $in: queryLabels };
-    });
-  }
+          if (boards.count()) {
+            boards.forEach(board => {
+              board.labels
+                .filter(boardLabel => {
+                  return boardLabel.name.match(reLabel);
+                })
+                .forEach(boardLabel => {
+                  queryLabels.push(boardLabel._id);
+                });
+            });
+          } else {
+            errors.notFound.labels.push(label);
+          }
+        }
 
-  let cards = null;
+        selector.labelIds = { $in: queryLabels };
+      });
+    }
 
-  if (!errors.hasErrors()) {
     if (queryParams.text) {
       const regex = new RegExp(escapeForRegex(queryParams.text), 'i');
 
@@ -508,12 +518,16 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
     if (selector.$and.length === 0) {
       delete selector.$and;
     }
+  }
 
-    // eslint-disable-next-line no-console
-    console.log('selector:', selector);
-    // eslint-disable-next-line no-console
-    console.log('selector.$and:', selector.$and);
+  // eslint-disable-next-line no-console
+  console.log('selector:', selector);
+  // eslint-disable-next-line no-console
+  console.log('selector.$and:', selector.$and);
 
+  let cards = null;
+
+  if (!errors.hasErrors()) {
     const projection = {
       fields: {
         _id: 1,
@@ -532,7 +546,8 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
         modifiedAt: 1,
         labelIds: 1,
       },
-      limit: 50,
+      skip,
+      limit,
     };
 
     if (queryParams.sort === 'due') {
@@ -569,27 +584,33 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
       };
     }
 
+    // eslint-disable-next-line no-console
+    console.log('projection:', projection);
     cards = Cards.find(selector, projection);
 
     // eslint-disable-next-line no-console
-    // console.log('count:', cards.count());
+    console.log('count:', cards.count());
   }
 
   const update = {
     $set: {
       totalHits: 0,
       lastHit: 0,
+      resultsCount: 0,
       cards: [],
       errors: errors.errorMessages(),
+      selector: JSON.stringify(selector),
     },
   };
 
   if (cards) {
     update.$set.totalHits = cards.count();
-    update.$set.lastHit = cards.count() > 50 ? 50 : cards.count();
+    update.$set.lastHit =
+      skip + limit < cards.count() ? skip + limit : cards.count();
     update.$set.cards = cards.map(card => {
       return card._id;
     });
+    update.$set.resultsCount = update.$set.cards.length;
   }
 
   SessionData.upsert({ userId, sessionId }, update);