From ff626fb559562af4bd06bc78bf438a7c8361390d Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 16 Jan 2021 19:20:31 +0200 Subject: [PATCH 01/10] Add a new SessionData collection and limit user fields * Add new SessionData collection to store user session data available to server and client * Limit the Users fields sent to the client by `myCards`, `dueCards`, `brokenCards`, and `globalSearch` using new `Users.safeFields` * clean-up --- client/components/main/globalSearch.js | 7 +- models/cards.js | 8 --- models/users.js | 8 +++ models/usersessiondata.js | 73 +++++++++++++++++++ server/publications/cards.js | 98 ++++++++------------------ 5 files changed, 113 insertions(+), 81 deletions(-) create mode 100644 models/usersessiondata.js diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 560cf9dda..f0a03cbdb 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -50,12 +50,11 @@ BlazeComponent.extendComponent({ results() { if (this.queryParams) { const results = Cards.globalSearch(this.queryParams); + const sessionData = SessionData.findOne({ userId: Meteor.userId() }); // eslint-disable-next-line no-console - // console.log('user:', Meteor.user()); - // eslint-disable-next-line no-console - // console.log('user:', Meteor.user().sessionData); + console.log('sessionData:', sessionData); // console.log('errors:', results.errors); - this.totalHits.set(Meteor.user().sessionData.totalHits); + this.totalHits.set(sessionData.totalHits); this.resultsCount.set(results.cards.count()); this.queryErrors.set(results.errors); return results.cards; diff --git a/models/cards.js b/models/cards.js index 04852cb9a..a2453535a 100644 --- a/models/cards.js +++ b/models/cards.js @@ -1914,14 +1914,6 @@ Cards.globalSearch = queryParams => { // eslint-disable-next-line no-console // console.log('count:', cards.count()); - if (Meteor.isServer) { - Users.update(userId, { - $set: { - 'sessionData.totalHits': cards.count(), - 'sessionData.lastHit': cards.count() > 50 ? 50 : cards.count(), - }, - }); - } return { cards, errors }; }; diff --git a/models/users.js b/models/users.js index 8b0c13230..d6bf22c27 100644 --- a/models/users.js +++ b/models/users.js @@ -377,6 +377,14 @@ Users.initEasySearch(searchInFields, { returnFields: [...searchInFields, 'profile.avatarUrl'], }); +Users.safeFields = { + _id: 1, + username: 1, + 'profile.fullname': 1, + 'profile.avatarUrl': 1, + 'profile.initials': 1, +}; + if (Meteor.isClient) { Users.helpers({ isBoardMember() { diff --git a/models/usersessiondata.js b/models/usersessiondata.js new file mode 100644 index 000000000..e93cde2bd --- /dev/null +++ b/models/usersessiondata.js @@ -0,0 +1,73 @@ +SessionData = new Mongo.Collection('sessiondata'); + +/** + * A UserSessionData in Wekan. Organization in Trello. + */ +SessionData.attachSchema( + new SimpleSchema({ + _id: { + /** + * the organization id + */ + type: Number, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + return incrementCounter('counters', 'orgId', 1); + } + }, + }, + userId: { + /** + * userId of the user + */ + type: String, + optional: false, + }, + totalHits: { + /** + * total number of hits in the last report query + */ + type: Number, + optional: true, + }, + lastHit: { + /** + * the last hit returned from a report query + */ + type: Number, + optional: true, + }, + createdAt: { + /** + * creation date of the team + */ + type: Date, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else if (this.isUpsert) { + return { $setOnInsert: new Date() }; + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + }), +); + +export default SessionData; diff --git a/server/publications/cards.js b/server/publications/cards.js index ba60d3dc1..abeffb0b4 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -72,18 +72,7 @@ Meteor.publish('myCards', function() { Boards.find({ _id: { $in: boards } }), Swimlanes.find({ _id: { $in: swimlanes } }), Lists.find({ _id: { $in: lists } }), - Users.find( - { _id: { $in: users } }, - { - fields: { - _id: 1, - username: 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - 'profile.initials': 1, - }, - }, - ), + Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), ]; }); @@ -93,18 +82,7 @@ Meteor.publish('dueCards', function(allUsers = false) { // eslint-disable-next-line no-console // console.log('all users:', allUsers); - const user = Users.findOne( - { _id: this.userId }, - { - fields: { - _id: 1, - username: 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - 'profile.initials': 1, - }, - }, - ); + const user = Users.findOne({ _id: this.userId }); const archivedBoards = []; Boards.find({ archived: true }).forEach(board => { @@ -115,14 +93,12 @@ Meteor.publish('dueCards', function(allUsers = false) { let selector = { archived: false, }; - // for admins and users, allow her to see cards only from boards where - // she is a member - //if (!user.isAdmin) { + selector.$or = [ { permission: 'public' }, { members: { $elemMatch: { userId: user._id, isActive: true } } }, ]; - //} + Boards.find(selector).forEach(board => { permiitedBoards.push(board._id); }); @@ -193,18 +169,7 @@ 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 } }, - { - fields: { - _id: 1, - username: 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - 'profile.initials': 1, - }, - }, - ), + Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), ]; }); @@ -216,6 +181,25 @@ Meteor.publish('globalSearch', function(queryParams) { const cards = Cards.globalSearch(queryParams).cards; + SessionData.upsert( + { userId: this.userId }, + { + $set: { + totalHits: cards.count(), + lastHit: cards.count() > 50 ? 50 : cards.count(), + }, + }, + ); + + // eslint-disable-next-line no-console + console.log('SessionData:', SessionData.find().fetch()); + // Users.update(this.userId, { + // $set: { + // 'sessionData.totalHits': cards.count(), + // 'sessionData.lastHit': cards.count() > 50 ? 50 : cards.count(), + // }, + // }); + const boards = []; const swimlanes = []; const lists = []; @@ -244,34 +228,21 @@ Meteor.publish('globalSearch', function(queryParams) { Boards.find({ _id: { $in: boards } }), Swimlanes.find({ _id: { $in: swimlanes } }), Lists.find({ _id: { $in: lists } }), - Users.find({ _id: { $in: users } }), + Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), + SessionData.find({ userId: this.userId }), ]; }); Meteor.publish('brokenCards', function() { - const user = Users.findOne( - { _id: this.userId }, - { - fields: { - _id: 1, - username: 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - 'profile.initials': 1, - }, - }, - ); + const user = Users.findOne({ _id: this.userId }); const permiitedBoards = [null]; let selector = {}; - // for admins and users, if user is not an admin allow her to see cards only from boards where - // she is a member - //if (!user.isAdmin) { selector.$or = [ { permission: 'public' }, { members: { $elemMatch: { userId: user._id, isActive: true } } }, ]; - //} + Boards.find(selector).forEach(board => { permiitedBoards.push(board._id); }); @@ -328,17 +299,6 @@ Meteor.publish('brokenCards', function() { Boards.find({ _id: { $in: boards } }), Swimlanes.find({ _id: { $in: swimlanes } }), Lists.find({ _id: { $in: lists } }), - Users.find( - { _id: { $in: users } }, - { - fields: { - _id: 1, - username: 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - 'profile.initials': 1, - }, - }, - ), + Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), ]; }); From 1e415b38d2f45d2d47ba7ac84025c8b154948708 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 16 Jan 2021 19:26:39 +0200 Subject: [PATCH 02/10] Fix URL reference in `resultCard` template --- client/components/cards/resultCard.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/cards/resultCard.jade b/client/components/cards/resultCard.jade index b1fabd9b6..77f0473af 100644 --- a/client/components/cards/resultCard.jade +++ b/client/components/cards/resultCard.jade @@ -1,6 +1,6 @@ template(name="resultCard") .result-card-wrapper - a.minicard-wrapper.card-title(href=card.absoluteUrl) + a.minicard-wrapper.card-title(href=absoluteUrl) +minicard(this) //= card.title ul.result-card-context-list From 7a5aa469cd7c55c149731bd3b567b62bf1b01654 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 16 Jan 2021 19:50:11 +0200 Subject: [PATCH 03/10] Modify search results translations * use named place holders in translation tag with multiple values --- client/components/main/globalSearch.jade | 9 +-------- client/components/main/globalSearch.js | 18 +++++++++++++++++- i18n/en.i18n.json | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index cdfcf5422..64dbe9211 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -20,14 +20,7 @@ template(name="globalSearch") else if hasResults.get .global-search-dueat-list-wrapper h1 - if $eq resultsCount.get 0 - | {{_ 'no-cards-found' }} - else if $eq resultsCount.get 1 - | {{_ 'one-card-found' }} - else if $eq resultsCount.get totalHits.get - | {{_ 'n-cards-found' resultsCount.get }} - else - | {{_ 'n-n-of-n-cards-found' 1 resultsCount.get totalHits.get }} + = resultsHeading if queryErrors.get div each msg in errorMessages diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index f0a03cbdb..2a1c9da32 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -52,7 +52,7 @@ BlazeComponent.extendComponent({ const results = Cards.globalSearch(this.queryParams); const sessionData = SessionData.findOne({ userId: Meteor.userId() }); // eslint-disable-next-line no-console - console.log('sessionData:', sessionData); + // console.log('sessionData:', sessionData); // console.log('errors:', results.errors); this.totalHits.set(sessionData.totalHits); this.resultsCount.set(results.cards.count()); @@ -83,6 +83,22 @@ BlazeComponent.extendComponent({ return messages; }, + resultsHeading() { + if (this.resultsCount.get() === 0) { + return TAPi18n.__('no-cards-found'); + } else if (this.resultsCount.get() === 1) { + return TAPi18n.__('one-card-found'); + } else if (this.resultsCount.get() === this.totalHits.get()) { + return TAPi18n.__('n-cards-found', this.resultsCount.get()); + } + + return TAPi18n.__('n-n-of-n-cards-found', { + start: 1, + end: this.resultsCount.get(), + total: this.totalHits.get(), + }); + }, + events() { return [ { diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index c4a22427e..6fac20a43 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -873,7 +873,7 @@ "no-cards-found": "No Cards Found", "one-card-found": "One Card Found", "n-cards-found": "%s Cards Found", - "n-n-of-n-cards-found": "%s-%s of %s Cards Found", + "n-n-of-n-cards-found": "__start__-__end__ of __total__ Cards Found", "operator-board": "board", "operator-board-abbrev": "b", "operator-swimlane": "swimlane", From 7b8d67de63de2f0bbc66b900dfc42a77f9c9452d Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 16 Jan 2021 21:07:49 +0200 Subject: [PATCH 04/10] Global search - fix label not found --- client/components/main/globalSearch.js | 3 +++ i18n/en.i18n.json | 1 + models/cards.js | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 2a1c9da32..0d9f1c80c 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -76,6 +76,9 @@ BlazeComponent.extendComponent({ errors.notFound.lists.forEach(list => { messages.push({ tag: 'list-title-not-found', value: list }); }); + errors.notFound.labels.forEach(label => { + messages.push({ tag: 'label-not-found', value: label }); + }); errors.notFound.users.forEach(user => { messages.push({ tag: 'user-username-not-found', value: user }); }); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 6fac20a43..9ad9fbcaf 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -868,6 +868,7 @@ "board-title-not-found": "Board '%s' not found.", "swimlane-title-not-found": "Swimlane '%s' not found.", "list-title-not-found": "List '%s' not found.", + "label-not-found": "Label '%s' not found.", "user-username-not-found": "Username '%s' not found.", "globalSearch-title": "Search All Boards", "no-cards-found": "No Cards Found", diff --git a/models/cards.js b/models/cards.js index a2453535a..dfc715f7b 100644 --- a/models/cards.js +++ b/models/cards.js @@ -1872,7 +1872,7 @@ Cards.globalSearch = queryParams => { }); }); } else { - errors.notFound.labels.push({ tag: 'label', value: label }); + errors.notFound.labels.push(label); } } From a3518a3bcfbc27f8d1db366d6b9bd17fe347b73e Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 16 Jan 2021 21:08:15 +0200 Subject: [PATCH 05/10] cleanup --- server/publications/cards.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/server/publications/cards.js b/server/publications/cards.js index abeffb0b4..8a3e0f2fc 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -191,15 +191,6 @@ Meteor.publish('globalSearch', function(queryParams) { }, ); - // eslint-disable-next-line no-console - console.log('SessionData:', SessionData.find().fetch()); - // Users.update(this.userId, { - // $set: { - // 'sessionData.totalHits': cards.count(), - // 'sessionData.lastHit': cards.count() > 50 ? 50 : cards.count(), - // }, - // }); - const boards = []; const swimlanes = []; const lists = []; From 8059856c39f85dfc035545571f515946327f24cd Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sun, 17 Jan 2021 00:05:45 +0200 Subject: [PATCH 06/10] Convert global search instructions to translatable tags --- client/components/main/globalSearch.jade | 22 +---------- client/components/main/globalSearch.js | 50 ++++++++++++++++++++++++ client/components/main/globalSearch.styl | 4 +- i18n/en.i18n.json | 18 ++++++++- 4 files changed, 70 insertions(+), 24 deletions(-) diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 64dbe9211..3b6daeb79 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -30,28 +30,8 @@ template(name="globalSearch") +resultCard(card) else .global-search-instructions - h1 Search Operators +viewer - = '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. `list:"To Review"`).\n' - = 'Available operators are:\n' - = '* `board:title` - cards in boards matching the specified title\n' - = '* `list:title` - cards in lists matching the specified title\n' - = '* `swimlane:title` - cards in swimlanes matching the specified title\n' - = '* `label:color` - cards that have a label matching the given color\n' - = '* `label:name` - cards that have a label matching the given name\n' - = '* `user:username` - cards where the specified user is a member or assignee\n' - = '* `@username` - shorthand for `user:username`\n' - = '* `#label` - shorthand for `label:color-or-name`\n' - = '## Notes\n' - = '* Multiple operators may be specified.\n' - = '* Similar operators are *OR*ed together. Cards that match any of the conditions will be returned.\n' - = ' `list:Available list:Blocked` would return cards contained in any list named *Blocked* or *Available*.\n' - = '* Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned.\n' - = '`list:Available label:red` returns only cards in the list *Available* with a *red* label.\n' - = '* Text searches are case insensitive.\n' + = searchInstructions template(name="globalSearchViewChangePopup") if currentUser diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 0d9f1c80c..77a413577 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -102,6 +102,56 @@ BlazeComponent.extendComponent({ }); }, + searchInstructions() { + tags = { + operator_board: TAPi18n.__('operator-board'), + operator_list: TAPi18n.__('operator-list'), + operator_swimlane: TAPi18n.__('operator-swimlane'), + operator_label: TAPi18n.__('operator-label'), + operator_label_abbrev: TAPi18n.__('operator-label-abbrev'), + operator_user: TAPi18n.__('operator-user'), + operator_user_abbrev: TAPi18n.__('operator-user-abbrev'), + }; + + text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; + text += `\n${TAPi18n.__('globalSearch-instructions-description', tags)}`; + text += `\n${TAPi18n.__('globalSearch-instructions-operators', tags)}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-board', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-list', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-swimlane', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-label', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-hash', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-user', + tags, + )}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-at', tags)}`; + + text += `\n## ${TAPi18n.__('heading-notes')}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-4', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-5', tags)}`; + + return text; + }, + events() { return [ { diff --git a/client/components/main/globalSearch.styl b/client/components/main/globalSearch.styl index 20bb45138..847ea018f 100644 --- a/client/components/main/globalSearch.styl +++ b/client/components/main/globalSearch.styl @@ -91,7 +91,7 @@ font-style: italic code - color: white - background-color: grey + color: black + background-color: lightgrey padding: 0.1rem !important font-size: 0.7rem !important diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 9ad9fbcaf..be74dfcfd 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -885,5 +885,21 @@ "operator-label-abbrev": "#", "operator-user": "user", "operator-user-abbrev": "@", - "operator-is": "is" + "operator-is": "is", + "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\"`).", + "globalSearch-instructions-operators": "Available operators:", + "globalSearch-instructions-operator-board": "`__operator_board__:title` - cards in boards matching the specified title", + "globalSearch-instructions-operator-list": "`__operator_list__:title` - cards in lists matching the specified title", + "globalSearch-instructions-operator-swimlane": "`__operator_swimlane__:title` - cards in swimlanes matching the specified title", + "globalSearch-instructions-operator-label": "`__operator_label__:color` `__operator_label__:name` - cards that have a label matching the given color or name", + "globalSearch-instructions-operator-hash": "`__operator_label_abbrev__label` - shorthand for `__operator_label__:label`", + "globalSearch-instructions-operator-user": "`__operator_user__:username` - cards where the specified user is a *member* or *assignee*", + "globalSearch-instructions-operator-at": "`__operator_user_abbrev__username` - shorthand for `user:username`", + "globalSearch-instructions-notes-1": "Multiple operators may be specified.", + "globalSearch-instructions-notes-2": "Similar operators are *OR*ed together. Cards that match any of the conditions will be returned.\n`__operator_list__:Available __operator_list__:Blocked` would return cards contained in any list named *Blocked* or *Available*.", + "globalSearch-instructions-notes-3": "Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned.\n`__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.", + "globalSearch-instructions-notes-4": "Text searches are case insensitive.", + "globalSearch-instructions-notes-5": "Currently archived cards are not searched." } From d74dc92681b67d0d7a2fd77e8e51c8c05ce9cc9d Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sun, 17 Jan 2021 16:01:42 +0200 Subject: [PATCH 07/10] Global Search improvements * support for searching from the URL * add support for searching by assignee and member --- client/components/main/globalSearch.jade | 15 +- client/components/main/globalSearch.js | 344 ++++++++++++++--------- client/components/main/globalSearch.styl | 2 +- config/router.js | 4 + i18n/en.i18n.json | 7 + models/cards.js | 85 +++++- server/publications/cards.js | 4 + 7 files changed, 299 insertions(+), 162 deletions(-) diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 3b6daeb79..0e3e801c4 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -14,20 +14,21 @@ template(name="globalSearch") if currentUser .wrapper form.global-search-instructions.js-search-query-form - input.global-search-query-input(type="text" name="searchQuery" placeholder="{{_ 'search-example'}}" autofocus dir="auto") + input.global-search-query-input(type="text" name="searchQuery" placeholder="{{_ 'search-example'}}" value="{{ query.get }}" autofocus dir="auto") if searching.get +spinner else if hasResults.get - .global-search-dueat-list-wrapper - h1 - = resultsHeading - if queryErrors.get + .global-search-results-list-wrapper + if hasQueryErrors.get div each msg in errorMessages span.global-search-error-messages | {{_ msg.tag msg.value }} - each card in results - +resultCard(card) + else + h1 + = resultsHeading.get + each card in results + +resultCard(card) else .global-search-instructions +viewer diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 77a413577..7a6f73d6f 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -36,69 +36,225 @@ BlazeComponent.extendComponent({ BlazeComponent.extendComponent({ onCreated() { - this.isPageReady = new ReactiveVar(true); this.searching = new ReactiveVar(false); this.hasResults = new ReactiveVar(false); + this.hasQueryErrors = new ReactiveVar(false); this.query = new ReactiveVar(''); + this.resultsHeading = new ReactiveVar(''); this.queryParams = null; - this.resultsCount = new ReactiveVar(0); - this.totalHits = new ReactiveVar(0); - this.queryErrors = new ReactiveVar(null); + this.parsingErrors = []; + this.resultsCount = 0; + this.totalHits = 0; + this.queryErrors = null; Meteor.subscribe('setting'); + if (Session.get('globalQuery')) { + // eslint-disable-next-line no-console + // console.log(Session.get('globalQuery')); + this.searchAllBoards(Session.get('globalQuery')); + } + }, + + resetSearch() { + this.searching.set(false); + this.hasResults.set(false); + this.hasQueryErrors.set(false); + this.resultsHeading.set(''); + this.parsingErrors = []; + this.resultsCount = 0; + this.totalHits = 0; + this.queryErrors = null; }, results() { + // eslint-disable-next-line no-console + console.log('getting results'); if (this.queryParams) { const results = Cards.globalSearch(this.queryParams); - const sessionData = SessionData.findOne({ userId: Meteor.userId() }); + this.queryErrors = results.errors; // eslint-disable-next-line no-console - // console.log('sessionData:', sessionData); - // console.log('errors:', results.errors); - this.totalHits.set(sessionData.totalHits); - this.resultsCount.set(results.cards.count()); - this.queryErrors.set(results.errors); - return results.cards; + console.log('errors:', this.queryErrors); + if (this.errorMessages().length) { + this.hasQueryErrors.set(true); + return null; + } + + if (results.cards) { + const sessionData = SessionData.findOne({ userId: Meteor.userId() }); + this.totalHits = sessionData.totalHits; + this.resultsCount = results.cards.count(); + this.resultsHeading.set(this.getResultsHeading()); + return results.cards; + } } - this.resultsCount.set(0); + this.resultsCount = 0; return []; }, errorMessages() { - const errors = this.queryErrors.get(); const messages = []; - errors.notFound.boards.forEach(board => { - messages.push({ tag: 'board-title-not-found', value: board }); - }); - errors.notFound.swimlanes.forEach(swim => { - messages.push({ tag: 'swimlane-title-not-found', value: swim }); - }); - errors.notFound.lists.forEach(list => { - messages.push({ tag: 'list-title-not-found', value: list }); - }); - errors.notFound.labels.forEach(label => { - messages.push({ tag: 'label-not-found', value: label }); - }); - errors.notFound.users.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); + if (this.queryErrors) { + this.queryErrors.notFound.boards.forEach(board => { + messages.push({ tag: 'board-title-not-found', value: board }); + }); + this.queryErrors.notFound.swimlanes.forEach(swim => { + messages.push({ tag: 'swimlane-title-not-found', value: swim }); + }); + this.queryErrors.notFound.lists.forEach(list => { + messages.push({ tag: 'list-title-not-found', value: list }); + }); + this.queryErrors.notFound.labels.forEach(label => { + messages.push({ tag: 'label-not-found', value: label }); + }); + this.queryErrors.notFound.users.forEach(user => { + messages.push({ tag: 'user-username-not-found', value: user }); + }); + this.queryErrors.notFound.members.forEach(user => { + messages.push({ tag: 'user-username-not-found', value: user }); + }); + this.queryErrors.notFound.assignees.forEach(user => { + messages.push({ tag: 'user-username-not-found', value: user }); + }); + } + + if (this.parsingErrors.length) { + this.parsingErrors.forEach(err => { + messages.push(err); + }); + } return messages; }, - resultsHeading() { - if (this.resultsCount.get() === 0) { + searchAllBoards(query) { + this.query.set(query); + + this.resetSearch(); + + if (!query) { + return; + } + + this.searching.set(true); + + // eslint-disable-next-line no-console + // console.log('query:', query); + + const reOperator1 = /^((?\w+):|(?[#@]))(?\w+)(\s+|$)/; + const reOperator2 = /^((?\w+):|(?[#@]))(?["']*)(?.*?)\k(\s+|$)/; + const reText = /^(?\S+)(\s+|$)/; + const reQuotedText = /^(?["'])(?\w+)\k(\s+|$)/; + + const operatorMap = {}; + operatorMap[TAPi18n.__('operator-board')] = 'boards'; + operatorMap[TAPi18n.__('operator-board-abbrev')] = 'boards'; + operatorMap[TAPi18n.__('operator-swimlane')] = 'swimlanes'; + operatorMap[TAPi18n.__('operator-swimlane-abbrev')] = 'swimlanes'; + operatorMap[TAPi18n.__('operator-list')] = 'lists'; + operatorMap[TAPi18n.__('operator-list-abbrev')] = 'lists'; + operatorMap[TAPi18n.__('operator-label')] = 'labels'; + operatorMap[TAPi18n.__('operator-label-abbrev')] = 'labels'; + operatorMap[TAPi18n.__('operator-user')] = 'users'; + operatorMap[TAPi18n.__('operator-user-abbrev')] = 'users'; + operatorMap[TAPi18n.__('operator-member')] = 'members'; + operatorMap[TAPi18n.__('operator-member-abbrev')] = 'members'; + operatorMap[TAPi18n.__('operator-assignee')] = 'assignees'; + operatorMap[TAPi18n.__('operator-assignee-abbrev')] = 'assignees'; + operatorMap[TAPi18n.__('operator-is')] = 'is'; + + // eslint-disable-next-line no-console + // console.log('operatorMap:', operatorMap); + const params = { + boards: [], + swimlanes: [], + lists: [], + users: [], + members: [], + assignees: [], + labels: [], + is: [], + }; + + let text = ''; + while (query) { + m = query.match(reOperator1); + if (!m) { + m = query.match(reOperator2); + if (m) { + query = query.replace(reOperator2, ''); + } + } else { + query = query.replace(reOperator1, ''); + } + if (m) { + let op; + if (m.groups.operator) { + op = m.groups.operator.toLowerCase(); + } else { + op = m.groups.abbrev; + } + if (op in operatorMap) { + params[operatorMap[op]].push(m.groups.value); + } else { + this.parsingErrors.push({ + tag: 'operator-unknown-error', + value: op, + }); + } + continue; + } + + m = query.match(reQuotedText); + if (!m) { + m = query.match(reText); + if (m) { + query = query.replace(reText, ''); + } + } else { + query = query.replace(reQuotedText, ''); + } + if (m) { + text += (text ? ' ' : '') + m.groups.text; + } + } + + // eslint-disable-next-line no-console + // console.log('text:', text); + params.text = text; + + // eslint-disable-next-line no-console + // console.log('params:', params); + + this.queryParams = params; + + this.autorun(() => { + const handle = subManager.subscribe('globalSearch', params); + Tracker.nonreactive(() => { + Tracker.autorun(() => { + // eslint-disable-next-line no-console + // console.log('ready:', handle.ready()); + if (handle.ready()) { + this.searching.set(false); + this.hasResults.set(true); + } + }); + }); + }); + }, + + getResultsHeading() { + if (this.resultsCount === 0) { return TAPi18n.__('no-cards-found'); - } else if (this.resultsCount.get() === 1) { + } else if (this.resultsCount === 1) { return TAPi18n.__('one-card-found'); - } else if (this.resultsCount.get() === this.totalHits.get()) { - return TAPi18n.__('n-cards-found', this.resultsCount.get()); + } else if (this.resultsCount === this.totalHits) { + return TAPi18n.__('n-cards-found', this.resultsCount); } return TAPi18n.__('n-n-of-n-cards-found', { start: 1, - end: this.resultsCount.get(), - total: this.totalHits.get(), + end: this.resultsCount, + total: this.totalHits, }); }, @@ -111,6 +267,10 @@ BlazeComponent.extendComponent({ operator_label_abbrev: TAPi18n.__('operator-label-abbrev'), operator_user: TAPi18n.__('operator-user'), operator_user_abbrev: TAPi18n.__('operator-user-abbrev'), + operator_member: TAPi18n.__('operator-member'), + operator_member_abbrev: TAPi18n.__('operator-member-abbrev'), + operator_assignee: TAPi18n.__('operator-assignee'), + operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'), }; text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; @@ -141,6 +301,14 @@ BlazeComponent.extendComponent({ tags, )}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-at', tags)}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-member', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-assignee', + tags, + )}`; text += `\n## ${TAPi18n.__('heading-notes')}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`; @@ -157,111 +325,7 @@ BlazeComponent.extendComponent({ { 'submit .js-search-query-form'(evt) { evt.preventDefault(); - this.query.set(evt.target.searchQuery.value); - this.queryErrors.set(null); - - if (!this.query.get()) { - this.searching.set(false); - this.hasResults.set(false); - return; - } - - this.searching.set(true); - this.hasResults.set(false); - - let query = this.query.get(); - // eslint-disable-next-line no-console - // console.log('query:', query); - - const reOperator1 = /^((?\w+):|(?[#@]))(?\w+)(\s+|$)/; - const reOperator2 = /^((?\w+):|(?[#@]))(?["']*)(?.*?)\k(\s+|$)/; - const reText = /^(?\S+)(\s+|$)/; - const reQuotedText = /^(?["'])(?\w+)\k(\s+|$)/; - - const operatorMap = {}; - operatorMap[TAPi18n.__('operator-board')] = 'boards'; - operatorMap[TAPi18n.__('operator-board-abbrev')] = 'boards'; - operatorMap[TAPi18n.__('operator-swimlane')] = 'swimlanes'; - operatorMap[TAPi18n.__('operator-swimlane-abbrev')] = 'swimlanes'; - operatorMap[TAPi18n.__('operator-list')] = 'lists'; - operatorMap[TAPi18n.__('operator-list-abbrev')] = 'lists'; - operatorMap[TAPi18n.__('operator-label')] = 'labels'; - operatorMap[TAPi18n.__('operator-label-abbrev')] = 'labels'; - operatorMap[TAPi18n.__('operator-user')] = 'users'; - operatorMap[TAPi18n.__('operator-user-abbrev')] = 'users'; - operatorMap[TAPi18n.__('operator-is')] = 'is'; - - // eslint-disable-next-line no-console - // console.log('operatorMap:', operatorMap); - const params = { - boards: [], - swimlanes: [], - lists: [], - users: [], - labels: [], - is: [], - }; - - let text = ''; - while (query) { - m = query.match(reOperator1); - if (!m) { - m = query.match(reOperator2); - if (m) { - query = query.replace(reOperator2, ''); - } - } else { - query = query.replace(reOperator1, ''); - } - if (m) { - let op; - if (m.groups.operator) { - op = m.groups.operator.toLowerCase(); - } else { - op = m.groups.abbrev; - } - if (op in operatorMap) { - params[operatorMap[op]].push(m.groups.value); - } - continue; - } - - m = query.match(reQuotedText); - if (!m) { - m = query.match(reText); - if (m) { - query = query.replace(reText, ''); - } - } else { - query = query.replace(reQuotedText, ''); - } - if (m) { - text += (text ? ' ' : '') + m.groups.text; - } - } - - // eslint-disable-next-line no-console - // console.log('text:', text); - params.text = text; - - // eslint-disable-next-line no-console - // console.log('params:', params); - - this.queryParams = params; - - this.autorun(() => { - const handle = subManager.subscribe('globalSearch', params); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - // eslint-disable-next-line no-console - // console.log('ready:', handle.ready()); - if (handle.ready()) { - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); + this.searchAllBoards(evt.target.searchQuery.value); }, }, ]; diff --git a/client/components/main/globalSearch.styl b/client/components/main/globalSearch.styl index 847ea018f..4dc5b5f6d 100644 --- a/client/components/main/globalSearch.styl +++ b/client/components/main/globalSearch.styl @@ -51,7 +51,7 @@ margin-top: 0 margin-bottom: 10px -.global-search-dueat-list-wrapper +.global-search-results-list-wrapper max-width: 500px margin-right: auto margin-left: auto diff --git a/config/router.js b/config/router.js index 4d5a386f9..faaa2aecb 100644 --- a/config/router.js +++ b/config/router.js @@ -158,7 +158,11 @@ FlowRouter.route('/global-search', { Utils.manageCustomUI(); Utils.manageMatomo(); + DocHead.setTitle(TAPi18n.__('globalSearch-title')); + // eslint-disable-next-line no-console + console.log('URL Params:', FlowRouter.getQueryParam('q')); + Session.set('globalQuery', decodeURI(FlowRouter.getQueryParam('q'))); BlazeLayout.render('defaultLayout', { headerBar: 'globalSearchHeaderBar', content: 'globalSearch', diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index be74dfcfd..90392f1eb 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -885,7 +885,12 @@ "operator-label-abbrev": "#", "operator-user": "user", "operator-user-abbrev": "@", + "operator-member": "member", + "operator-member-abbrev": "m", + "operator-assignee": "assignee", + "operator-assignee-abbrev": "a", "operator-is": "is", + "operator-unknown-error": "%s is not an operator", "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\"`).", @@ -897,6 +902,8 @@ "globalSearch-instructions-operator-hash": "`__operator_label_abbrev__label` - shorthand for `__operator_label__:label`", "globalSearch-instructions-operator-user": "`__operator_user__:username` - cards where the specified user is a *member* or *assignee*", "globalSearch-instructions-operator-at": "`__operator_user_abbrev__username` - shorthand for `user:username`", + "globalSearch-instructions-operator-member": "`__operator_member__:username` - cards where the specified user is a *member*", + "globalSearch-instructions-operator-assignee": "`__operator_assignee__:username` - cards where the specified user is an *assignee*", "globalSearch-instructions-notes-1": "Multiple operators may be specified.", "globalSearch-instructions-notes-2": "Similar operators are *OR*ed together. Cards that match any of the conditions will be returned.\n`__operator_list__:Available __operator_list__:Blocked` would return cards contained in any list named *Blocked* or *Available*.", "globalSearch-instructions-notes-3": "Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned.\n`__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.", diff --git a/models/cards.js b/models/cards.js index dfc715f7b..55c601c3b 100644 --- a/models/cards.js +++ b/models/cards.js @@ -1735,16 +1735,31 @@ Cards.globalSearch = queryParams => { // eslint-disable-next-line no-console // console.log('userId:', userId); - const errors = { - notFound: { - boards: [], - swimlanes: [], - lists: [], - labels: [], - users: [], - is: [], - }, - }; + const errors = new (class { + constructor() { + this.notFound = { + boards: [], + swimlanes: [], + lists: [], + labels: [], + users: [], + members: [], + assignees: [], + is: [], + }; + } + + hasErrors() { + for (const prop in this.notFound) { + if (this.notFound[prop].length) { + // eslint-disable-next-line no-console + console.log('errors in:', prop, this.notFound[prop]); + return true; + } + } + return false; + } + })(); const selector = { archived: false, @@ -1808,25 +1823,63 @@ Cards.globalSearch = queryParams => { selector.listId.$in = queryLists; } + const queryMembers = []; + const queryAssignees = []; if (queryParams.users.length) { - const queryUsers = []; queryParams.users.forEach(query => { const users = Users.find({ username: query, }); if (users.count()) { users.forEach(user => { - queryUsers.push(user._id); + queryMembers.push(user._id); + queryAssignees.push(user._id); }); } 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); + }); + } 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); + }); + } else { + errors.notFound.assignees.push(query); + } + }); + } + + if (queryMembers.length && queryAssignees.length) { selector.$or = [ - { members: { $in: queryUsers } }, - { assignees: { $in: queryUsers } }, + { members: { $in: queryMembers } }, + { assignees: { $in: queryAssignees } }, ]; + } else if (queryMembers.length) { + selector.members = { $in: queryMembers }; + } else if (queryAssignees.length) { + selector.assignees = { $in: queryAssignees }; } if (queryParams.labels.length) { @@ -1880,6 +1933,10 @@ Cards.globalSearch = queryParams => { }); } + if (errors.hasErrors()) { + return { cards: null, errors }; + } + if (queryParams.text) { const regex = new RegExp(queryParams.text, 'i'); diff --git a/server/publications/cards.js b/server/publications/cards.js index 8a3e0f2fc..6d5223c50 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -181,6 +181,10 @@ Meteor.publish('globalSearch', function(queryParams) { const cards = Cards.globalSearch(queryParams).cards; + if (!cards) { + return []; + } + SessionData.upsert( { userId: this.userId }, { From 7e8475e64db49e82d6880b66cffe805533b98fd7 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sun, 17 Jan 2021 21:04:05 +0200 Subject: [PATCH 08/10] Add link to search header for storing search --- client/components/main/globalSearch.jade | 1 + client/components/main/globalSearch.js | 6 ++++++ config/router.js | 7 ++++--- i18n/en.i18n.json | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 0e3e801c4..961c5b436 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -27,6 +27,7 @@ template(name="globalSearch") else h1 = resultsHeading.get + a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}") each card in results +resultCard(card) else diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 7a6f73d6f..72f8d4380 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -41,6 +41,7 @@ BlazeComponent.extendComponent({ this.hasQueryErrors = new ReactiveVar(false); this.query = new ReactiveVar(''); this.resultsHeading = new ReactiveVar(''); + this.searchLink = new ReactiveVar(null); this.queryParams = null; this.parsingErrors = []; this.resultsCount = 0; @@ -258,6 +259,11 @@ BlazeComponent.extendComponent({ }); }, + getSearchHref() { + const baseUrl = window.location.href.replace(/([?#].*$|\s*$)/, ''); + return `${baseUrl}?q=${encodeURIComponent(this.query.get())}`; + }, + searchInstructions() { tags = { operator_board: TAPi18n.__('operator-board'), diff --git a/config/router.js b/config/router.js index faaa2aecb..d41097c5f 100644 --- a/config/router.js +++ b/config/router.js @@ -160,9 +160,10 @@ FlowRouter.route('/global-search', { Utils.manageMatomo(); DocHead.setTitle(TAPi18n.__('globalSearch-title')); - // eslint-disable-next-line no-console - console.log('URL Params:', FlowRouter.getQueryParam('q')); - Session.set('globalQuery', decodeURI(FlowRouter.getQueryParam('q'))); + Session.set( + 'globalQuery', + decodeURIComponent(FlowRouter.getQueryParam('q')), + ); BlazeLayout.render('defaultLayout', { headerBar: 'globalSearchHeaderBar', content: 'globalSearch', diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 90392f1eb..b1859ff2b 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -908,5 +908,6 @@ "globalSearch-instructions-notes-2": "Similar operators are *OR*ed together. Cards that match any of the conditions will be returned.\n`__operator_list__:Available __operator_list__:Blocked` would return cards contained in any list named *Blocked* or *Available*.", "globalSearch-instructions-notes-3": "Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned.\n`__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.", "globalSearch-instructions-notes-4": "Text searches are case insensitive.", - "globalSearch-instructions-notes-5": "Currently archived cards are not searched." + "globalSearch-instructions-notes-5": "Currently archived cards are not searched.", + "link-to-search": "Link to this search" } From e70a1f0a603c80231698ff77e2c1f761c6174637 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sun, 17 Jan 2021 21:18:45 +0200 Subject: [PATCH 09/10] Fix bug when no search param specified --- config/router.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/config/router.js b/config/router.js index d41097c5f..6aca98570 100644 --- a/config/router.js +++ b/config/router.js @@ -160,10 +160,12 @@ FlowRouter.route('/global-search', { Utils.manageMatomo(); DocHead.setTitle(TAPi18n.__('globalSearch-title')); - Session.set( - 'globalQuery', - decodeURIComponent(FlowRouter.getQueryParam('q')), - ); + if (FlowRouter.getQueryParam('q')) { + Session.set( + 'globalQuery', + decodeURIComponent(FlowRouter.getQueryParam('q')), + ); + } BlazeLayout.render('defaultLayout', { headerBar: 'globalSearchHeaderBar', content: 'globalSearch', From b5124d0f6a0452bbbf41c3d82214bb4e368d791e Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sun, 17 Jan 2021 21:43:56 +0200 Subject: [PATCH 10/10] cleanup debug code --- client/components/main/globalSearch.js | 6 ++---- models/cards.js | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 72f8d4380..51428c2e2 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -49,8 +49,6 @@ BlazeComponent.extendComponent({ this.queryErrors = null; Meteor.subscribe('setting'); if (Session.get('globalQuery')) { - // eslint-disable-next-line no-console - // console.log(Session.get('globalQuery')); this.searchAllBoards(Session.get('globalQuery')); } }, @@ -68,12 +66,12 @@ BlazeComponent.extendComponent({ results() { // eslint-disable-next-line no-console - console.log('getting results'); + // console.log('getting results'); if (this.queryParams) { const results = Cards.globalSearch(this.queryParams); this.queryErrors = results.errors; // eslint-disable-next-line no-console - console.log('errors:', this.queryErrors); + // console.log('errors:', this.queryErrors); if (this.errorMessages().length) { this.hasQueryErrors.set(true); return null; diff --git a/models/cards.js b/models/cards.js index 7e902eb40..5358f41b7 100644 --- a/models/cards.js +++ b/models/cards.js @@ -1883,8 +1883,6 @@ Cards.globalSearch = queryParams => { hasErrors() { for (const prop in this.notFound) { if (this.notFound[prop].length) { - // eslint-disable-next-line no-console - console.log('errors in:', prop, this.notFound[prop]); return true; } }