浏览代码

Merge pull request #3597 from jrsupplee/search

Global Search Updates
Lauri Ojansivu 4 年之前
父节点
当前提交
4f9b4059a6
共有 5 个文件被更改,包括 119 次插入18 次删除
  1. 35 0
      client/components/main/globalSearch.js
  2. 11 1
      i18n/en.i18n.json
  3. 11 9
      models/boards.js
  4. 7 2
      models/usersessiondata.js
  5. 55 6
      server/publications/cards.js

+ 35 - 0
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 = {
@@ -238,12 +239,19 @@ BlazeComponent.extendComponent({
         'predicate-archived': 'archived',
         'predicate-all': 'all',
         'predicate-ended': 'ended',
+        'predicate-public': 'public',
+        'predicate-private': 'private',
       },
       sorts: {
         'predicate-due': 'dueAt',
         '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 +284,7 @@ BlazeComponent.extendComponent({
       createdAt: null,
       modifiedAt: null,
       comments: [],
+      has: [],
     };
 
     let text = '';
@@ -296,6 +305,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 +363,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);
@@ -511,6 +530,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'),
@@ -519,6 +539,11 @@ 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'),
+      predicate_public: TAPi18n.__('predicate-public'),
+      predicate_private: TAPi18n.__('predicate-private'),
     };
 
     text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`;
@@ -574,9 +599,19 @@ 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)}`;
 
+    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)}`;

+ 11 - 1
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,16 @@
   "predicate-due": "due",
   "predicate-modified": "modified",
   "predicate-created": "created",
+  "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",
   "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",
@@ -943,12 +950,15 @@
   "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*.",
   "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",

+ 11 - 9
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() {

+ 7 - 2
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',

+ 55 - 6
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;
@@ -341,6 +353,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
         }
       });
 
+      // eslint-disable-next-line no-prototype-builtins
       if (!selector.swimlaneId.hasOwnProperty('swimlaneId')) {
         selector.swimlaneId = { $in: [] };
       }
@@ -362,6 +375,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
         }
       });
 
+      // eslint-disable-next-line no-prototype-builtins
       if (!selector.hasOwnProperty('listId')) {
         selector.listId = { $in: [] };
       }
@@ -505,9 +519,39 @@ 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');
 
+      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 },
@@ -520,6 +564,8 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
               ),
             },
           },
+          { _id: { $in: checklists.map(list => list.cardId) } },
+          { _id: { $in: attachments.map(attach => attach.cardId) } },
         ],
       });
     }
@@ -686,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 }),
     ];
   }