From 4e8fc464755067f6e308f32dd091e7d76f41febe Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Wed, 27 Jan 2021 02:21:12 +0200 Subject: [PATCH] Start work on paging search results --- client/components/main/globalSearch.jade | 6 + client/components/main/globalSearch.js | 91 ++++- i18n/en.i18n.json | 2 + models/usersessiondata.js | 12 + server/publications/cards.js | 429 ++++++++++++----------- 5 files changed, 329 insertions(+), 211 deletions(-) diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 896954743..c0351f4e4 100644 --- a/client/components/main/globalSearch.jade +++ b/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' }} diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 908da2dcd..4da421e67 100644 --- a/client/components/main/globalSearch.js +++ b/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'); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index f54c42e0e..accf914f4 100644 --- a/i18n/en.i18n.json +++ b/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\"`).", diff --git a/models/usersessiondata.js b/models/usersessiondata.js index 59be52b3a..8309cf038 100644 --- a/models/usersessiondata.js +++ b/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, diff --git a/server/publications/cards.js b/server/publications/cards.js index 12ceec90e..7ed1bd597 100644 --- a/server/publications/cards.js +++ b/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);