diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 5359bf34f..c8042dbee 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -222,6 +222,7 @@ BlazeComponent.extendComponent({ 'operator-created': 'createdAt', 'operator-modified': 'modifiedAt', 'operator-comment': 'comments', + 'operator-has': 'has', }; const predicates = { @@ -244,6 +245,11 @@ BlazeComponent.extendComponent({ 'predicate-created': 'createdAt', 'predicate-modified': 'modifiedAt', }, + has: { + 'predicate-description': 'description', + 'predicate-checklist': 'checklist', + 'predicate-attachment': 'attachment', + }, }; const predicateTranslations = {}; Object.entries(predicates).forEach(([category, catPreds]) => { @@ -276,6 +282,7 @@ BlazeComponent.extendComponent({ createdAt: null, modifiedAt: null, comments: [], + has: [], }; let text = ''; @@ -296,6 +303,7 @@ BlazeComponent.extendComponent({ } else { op = m.groups.abbrev.toLowerCase(); } + // eslint-disable-next-line no-prototype-builtins if (operatorMap.hasOwnProperty(op)) { let value = m.groups.value; if (operatorMap[op] === 'labels') { @@ -353,6 +361,15 @@ BlazeComponent.extendComponent({ } else { value = predicateTranslations.status[value]; } + } else if (operatorMap[op] === 'has') { + if (!predicateTranslations.has[value]) { + this.parsingErrors.push({ + tag: 'operator-has-invalid', + value, + }); + } else { + value = predicateTranslations.has[value]; + } } if (Array.isArray(params[operatorMap[op]])) { params[operatorMap[op]].push(value); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 22fb31f97..d4e5cc712 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -906,6 +906,7 @@ "operator-modified": "modified", "operator-sort": "sort", "operator-comment": "comment", + "operator-has": "has", "predicate-archived": "archived", "predicate-ended": "ended", "predicate-all": "all", @@ -917,10 +918,14 @@ "predicate-due": "due", "predicate-modified": "modified", "predicate-created": "created", + "predicate-attachment": "attachment", + "predicate-description": "description", + "predicate-checklist": "checklist", "operator-unknown-error": "%s is not an operator", "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", + "operator-has-invalid": "%s is not a valid existence check", "next-page": "Next Page", "previous-page": "Previous Page", "heading-notes": "Notes", diff --git a/models/usersessiondata.js b/models/usersessiondata.js index 129fe56ba..003b35c91 100644 --- a/models/usersessiondata.js +++ b/models/usersessiondata.js @@ -134,7 +134,10 @@ SessionData.helpers({ SessionData.unpickle = pickle => { return JSON.parse(pickle, (key, value) => { - if (typeof value === 'object') { + if (value === null) { + return null; + } else if (typeof value === 'object') { + // eslint-disable-next-line no-prototype-builtins if (value.hasOwnProperty('$$class')) { if (value.$$class === 'RegExp') { return new RegExp(value.source, value.flags); @@ -147,7 +150,9 @@ SessionData.unpickle = pickle => { SessionData.pickle = value => { return JSON.stringify(value, (key, value) => { - if (typeof value === 'object') { + if (value === null) { + return null; + } else if (typeof value === 'object') { if (value.constructor.name === 'RegExp') { return { $$class: 'RegExp', diff --git a/server/publications/cards.js b/server/publications/cards.js index 2f9b64577..77a5734fb 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -507,6 +507,20 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { }); } + if (queryParams.has.length) { + queryParams.has.forEach(has => { + if (has === 'description') { + selector.description = { $exists: true, $nin: [null, ''] }; + } else if (has === 'attachment') { + const attachments = Attachments.find({}, { fields: { cardId: 1 } }); + selector.$and.push({ _id: { $in: attachments.map(a => a.cardId) } }); + } else if (has === 'checklist') { + const checklists = Checklists.find({}, { fields: { cardId: 1 } }); + selector.$and.push({ _id: { $in: checklists.map(a => a.cardId) } }); + } + }); + } + if (queryParams.text) { const regex = new RegExp(escapeForRegex(queryParams.text), 'i');