From 5b51a36c624c49a79fefbdc26c45c0f09c24b1ad Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 20 Feb 2021 18:35:44 +0200 Subject: [PATCH 1/6] Add searching of checklists --- server/publications/cards.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/publications/cards.js b/server/publications/cards.js index 7353f1eb3..baad7d447 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -507,7 +507,13 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { if (queryParams.text) { const regex = new RegExp(escapeForRegex(queryParams.text), 'i'); - + const items = ChecklistItems.find({ title: regex }); + const checklists = Checklists.find({ + $or: [ + { title: regex }, + { _id: { $in: items.map(item => item.checklistId) } }, + ], + }); selector.$and.push({ $or: [ { title: regex }, @@ -520,6 +526,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { ), }, }, + { _id: { $in: checklists.map(list => list.cardId) } }, ], }); } From c02b71e0e10d5f350aaad39764b54034643d0817 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sun, 21 Feb 2021 01:39:34 +0200 Subject: [PATCH 2/6] Add searching of attachment names --- server/publications/cards.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/server/publications/cards.js b/server/publications/cards.js index baad7d447..2f9b64577 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -341,6 +341,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { } }); + // eslint-disable-next-line no-prototype-builtins if (!selector.swimlaneId.hasOwnProperty('swimlaneId')) { selector.swimlaneId = { $in: [] }; } @@ -362,6 +363,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { } }); + // eslint-disable-next-line no-prototype-builtins if (!selector.hasOwnProperty('listId')) { selector.listId = { $in: [] }; } @@ -507,13 +509,23 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { if (queryParams.text) { const regex = new RegExp(escapeForRegex(queryParams.text), 'i'); - const items = ChecklistItems.find({ title: regex }); - const checklists = Checklists.find({ - $or: [ - { title: regex }, - { _id: { $in: items.map(item => item.checklistId) } }, - ], - }); + + const items = ChecklistItems.find( + { title: regex }, + { fields: { cardId: 1 } }, + ); + const checklists = Checklists.find( + { + $or: [ + { title: regex }, + { _id: { $in: items.map(item => item.checklistId) } }, + ], + }, + { fields: { cardId: 1 } }, + ); + + const attachments = Attachments.find({ 'original.name': regex }); + selector.$and.push({ $or: [ { title: regex }, @@ -527,6 +539,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { }, }, { _id: { $in: checklists.map(list => list.cardId) } }, + { _id: { $in: attachments.map(attach => attach.cardId) } }, ], }); } From 726be664c8388296aef1f522fc167bc2ade7e4be Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sun, 21 Feb 2021 01:41:58 +0200 Subject: [PATCH 3/6] Add new `has` operator for searching --- client/components/main/globalSearch.js | 17 +++++++++++++++++ i18n/en.i18n.json | 5 +++++ models/usersessiondata.js | 9 +++++++-- server/publications/cards.js | 14 ++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) 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'); From a8a9ee91bfd9815d3659d8abb456c8cd65a96b4e Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sun, 21 Feb 2021 19:57:35 +0200 Subject: [PATCH 4/6] Add help instructions for new `has` operator --- client/components/main/globalSearch.js | 6 ++++++ i18n/en.i18n.json | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index c8042dbee..d2c7a25fc 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -528,6 +528,7 @@ BlazeComponent.extendComponent({ operator_created: TAPi18n.__('operator-created'), operator_modified: TAPi18n.__('operator-modified'), operator_status: TAPi18n.__('operator-status'), + operator_has: TAPi18n.__('operator-has'), predicate_overdue: TAPi18n.__('predicate-overdue'), predicate_archived: TAPi18n.__('predicate-archived'), predicate_all: TAPi18n.__('predicate-all'), @@ -536,6 +537,9 @@ BlazeComponent.extendComponent({ predicate_month: TAPi18n.__('predicate-month'), predicate_quarter: TAPi18n.__('predicate-quarter'), predicate_year: TAPi18n.__('predicate-year'), + predicate_attachment: TAPi18n.__('predicate-attachment'), + predicate_description: TAPi18n.__('predicate-description'), + predicate_checklist: TAPi18n.__('predicate-checklist'), }; text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; @@ -594,6 +598,8 @@ BlazeComponent.extendComponent({ text += `\n* ${TAPi18n.__('globalSearch-instructions-status-all', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-status-ended', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-has', tags)}`; + text += `\n## ${TAPi18n.__('heading-notes')}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index d4e5cc712..c1e39cf26 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -948,12 +948,13 @@ "globalSearch-instructions-status-archived": "`__operator_status__:__predicate_archived__` - cards that are archived.", "globalSearch-instructions-status-all": "`__operator_status__:__predicate_all__` - all archived and unarchived cards.", "globalSearch-instructions-status-ended": "`__operator_status__:__predicate_ended__` - cards with an end date.", + "globalSearch-instructions-operator-has": "`__operator_has__:field` - where *field* is one of `__predicate_attachment__`, `__predicate_checklist__` or `__predicate_description__`", "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. `__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.", "globalSearch-instructions-notes-3-2": "Days can be specified as an integer or using `__predicate_week__`, `__predicate_month__`, `__predicate_quarter__` or `__predicate_year__`", "globalSearch-instructions-notes-4": "Text searches are case insensitive.", - "globalSearch-instructions-notes-5": "Currently archived cards are not searched.", + "globalSearch-instructions-notes-5": "By default archived cards are not searched.", "link-to-search": "Link to this search", "excel-font": "Arial", "number": "Number", From b419e17cacc213c571eb26ae17a0bd5bf0a053c1 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Tue, 23 Feb 2021 17:49:32 +0200 Subject: [PATCH 5/6] Only show label names on global search for type='board' --- models/boards.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/models/boards.js b/models/boards.js index cfe0e2524..c56265f52 100644 --- a/models/boards.js +++ b/models/boards.js @@ -1420,15 +1420,17 @@ if (Meteor.isServer) { }, myLabelNames() { let names = []; - Boards.userBoards(Meteor.userId()).forEach(board => { - names = names.concat( - board.labels - .filter(label => !!label.name) - .map(label => { - return label.name; - }), - ); - }); + Boards.userBoards(Meteor.userId(), false, { type: 'board' }).forEach( + board => { + names = names.concat( + board.labels + .filter(label => !!label.name) + .map(label => { + return label.name; + }), + ); + }, + ); return _.uniq(names).sort(); }, myBoardNames() { From c7276ee61490ba8754d62a5b58c29c7366ab6234 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Tue, 23 Feb 2021 17:56:28 +0200 Subject: [PATCH 6/6] Add new status predicates of public and private --- client/components/main/globalSearch.js | 12 ++++++++++++ i18n/en.i18n.json | 4 ++++ server/publications/cards.js | 27 ++++++++++++++++++++------ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index d2c7a25fc..a6013baf2 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -239,6 +239,8 @@ BlazeComponent.extendComponent({ 'predicate-archived': 'archived', 'predicate-all': 'all', 'predicate-ended': 'ended', + 'predicate-public': 'public', + 'predicate-private': 'private', }, sorts: { 'predicate-due': 'dueAt', @@ -540,6 +542,8 @@ BlazeComponent.extendComponent({ predicate_attachment: TAPi18n.__('predicate-attachment'), predicate_description: TAPi18n.__('predicate-description'), predicate_checklist: TAPi18n.__('predicate-checklist'), + predicate_public: TAPi18n.__('predicate-public'), + predicate_private: TAPi18n.__('predicate-private'), }; text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; @@ -595,6 +599,14 @@ BlazeComponent.extendComponent({ 'globalSearch-instructions-status-archived', tags, )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-status-public', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-status-private', + tags, + )}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-status-all', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-status-ended', tags)}`; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index c1e39cf26..ffb4d0f2c 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -921,6 +921,8 @@ "predicate-attachment": "attachment", "predicate-description": "description", "predicate-checklist": "checklist", + "predicate-public": "public", + "predicate-private": "private", "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", @@ -948,6 +950,8 @@ "globalSearch-instructions-status-archived": "`__operator_status__:__predicate_archived__` - cards that are archived.", "globalSearch-instructions-status-all": "`__operator_status__:__predicate_all__` - all archived and unarchived cards.", "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-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*.", diff --git a/server/publications/cards.js b/server/publications/cards.js index 77a5734fb..e61dce1f1 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -263,6 +263,8 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { if (queryParams.selector) { selector = queryParams.selector; } else { + const boardsSelector = {}; + let archived = false; let endAt = null; if (queryParams.status.length) { @@ -273,6 +275,8 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { archived = null; } else if (status === 'ended') { endAt = { $nin: [null, ''] }; + } else if (['private', 'public'].includes(status)) { + boardsSelector.permission = status; } }); } @@ -282,27 +286,35 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { $and: [], }; - const boardsSelector = {}; if (archived !== null) { - boardsSelector.archived = archived; if (archived) { - selector.boardId = { $in: Boards.userBoardIds(userId, null) }; + selector.boardId = { + $in: Boards.userBoardIds(userId, null, boardsSelector), + }; selector.$and.push({ $or: [ - { boardId: { $in: Boards.userBoardIds(userId, archived) } }, + { + boardId: { + $in: Boards.userBoardIds(userId, archived, boardsSelector), + }, + }, { swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } }, { listId: { $in: Lists.archivedListIds() } }, { archived: true }, ], }); } else { - selector.boardId = { $in: Boards.userBoardIds(userId, false) }; + selector.boardId = { + $in: Boards.userBoardIds(userId, false, boardsSelector), + }; selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() }; selector.listId = { $nin: Lists.archivedListIds() }; selector.archived = false; } } else { - selector.boardId = { $in: Boards.userBoardIds(userId, null) }; + selector.boardId = { + $in: Boards.userBoardIds(userId, null, boardsSelector), + }; } if (endAt !== null) { selector.endAt = endAt; @@ -720,6 +732,9 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { Lists.find({ _id: { $in: lists } }, { fields }), CustomFields.find({ _id: { $in: customFieldIds } }), Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), + Checklists.find({ cardId: { $in: cards.map(c => c._id) } }), + Attachments.find({ cardId: { $in: cards.map(c => c._id) } }), + CardComments.find({ cardId: { $in: cards.map(c => c._id) } }), SessionData.find({ userId: this.userId, sessionId }), ]; }