diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 154356d2d..a251fda04 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -243,6 +243,7 @@ BlazeComponent.extendComponent({ status: { 'predicate-archived': 'archived', 'predicate-all': 'all', + 'predicate-open': 'open', 'predicate-ended': 'ended', 'predicate-public': 'public', 'predicate-private': 'private', @@ -256,6 +257,11 @@ BlazeComponent.extendComponent({ 'predicate-description': 'description', 'predicate-checklist': 'checklist', 'predicate-attachment': 'attachment', + 'predicate-start': 'startAt', + 'predicate-end': 'endAt', + 'predicate-due': 'dueAt', + 'predicate-assignee': 'assignees', + 'predicate-member': 'members', }, }; const predicateTranslations = {}; @@ -423,13 +429,22 @@ BlazeComponent.extendComponent({ value = predicateTranslations.status[value]; } } else if (operator === 'has') { + let negated = false; + const m = value.match(reNegatedOperator); + if (m) { + value = m.groups.operator; + negated = true; + } if (!predicateTranslations.has[value]) { this.parsingErrors.push({ tag: 'operator-has-invalid', value, }); } else { - value = predicateTranslations.has[value]; + value = { + field: predicateTranslations.has[value], + exists: !negated, + }; } } else if (operator === 'limit') { const limit = parseInt(value, 10); @@ -597,6 +612,10 @@ BlazeComponent.extendComponent({ predicate_due: TAPi18n.__('predicate-due'), predicate_created: TAPi18n.__('predicate-created'), predicate_modified: TAPi18n.__('predicate-modified'), + predicate_start: TAPi18n.__('predicate-start'), + predicate_end: TAPi18n.__('predicate-end'), + predicate_assignee: TAPi18n.__('predicate-assignee'), + predicate_member: TAPi18n.__('predicate-member'), }; text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 13a6849c1..cc23a9a37 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -909,6 +909,7 @@ "operator-has": "has", "operator-limit": "limit", "predicate-archived": "archived", + "predicate-open": "open", "predicate-ended": "ended", "predicate-all": "all", "predicate-overdue": "overdue", @@ -922,6 +923,10 @@ "predicate-attachment": "attachment", "predicate-description": "description", "predicate-checklist": "checklist", + "predicate-start": "start", + "predicate-end": "end", + "predicate-assignee": "assignee", + "predicate-member": "member", "predicate-public": "public", "predicate-private": "private", "operator-unknown-error": "%s is not an operator", @@ -954,7 +959,7 @@ "globalSearch-instructions-status-ended": "`__operator_status__:__predicate_ended__` - cards with an end date.", "globalSearch-instructions-status-public": "`__operator_status__:__predicate_public__` - cards only in public boards.", "globalSearch-instructions-status-private": "`__operator_status__:__predicate_private__` - cards only in private boards.", - "globalSearch-instructions-operator-has": "`__operator_has__:field` - where *field* is one of `__predicate_attachment__`, `__predicate_checklist__` or `__predicate_description__`", + "globalSearch-instructions-operator-has": "`__operator_has__:field` - where *field* is one of `__predicate_attachment__`, `__predicate_checklist__`, `__predicate_description__`, `__predicate_start__`, `__predicate_due__`, `__predicate_end__`, `__predicate_assignee__` or `__predicate_member__`", "globalSearch-instructions-operator-sort": "`__operator_sort__:sort-name` - where *sort-name* is one of `__predicate_due__`, `__predicate_created__` or `__predicate_modified__`.", "globalSearch-instructions-operator-limit": "`__operator_limit__:n` - where *n* is the number of cards to be displayed per page expressed as a positive integer.", "globalSearch-instructions-notes-1": "Multiple operators may be specified.", diff --git a/server/publications/cards.js b/server/publications/cards.js index 062f10e2c..4197fd826 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -518,14 +518,33 @@ 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) } }); + switch (has.field) { + case 'attachment': + const attachments = Attachments.find({}, { fields: { cardId: 1 } }); + selector.$and.push({ _id: { $in: attachments.map(a => a.cardId) } }); + break; + case 'checklist': + const checklists = Checklists.find({}, { fields: { cardId: 1 } }); + selector.$and.push({ _id: { $in: checklists.map(a => a.cardId) } }); + break; + case 'description': + case 'startAt': + case 'dueAt': + case 'endAt': + if (has.exists) { + selector[has.field] = { $exists: true, $nin: [null, ''] }; + } else { + selector[has.field] = { $in: [null, ''] }; + } + break; + case 'assignees': + case 'members': + if (has.exists) { + selector[has.field] = { $exists: true, $nin: [null, []] }; + } else { + selector[has.field] = { $in: [null, []] }; + } + break; } }); }