Просмотр исходного кода

Merge branch 'jrsupplee-search'

Lauri Ojansivu 4 лет назад
Родитель
Сommit
4ea4913d72

+ 18 - 6
client/components/cards/resultCard.jade

@@ -6,8 +6,12 @@ template(name="resultCard")
     ul.result-card-context-list
     ul.result-card-context-list
       li.result-card-context(title="{{_ 'board'}}")
       li.result-card-context(title="{{_ 'board'}}")
         .result-card-block-wrapper
         .result-card-block-wrapper
-          +viewer
-            = getBoard.title
+          if boardId
+            +viewer
+              = getBoard.title
+          else
+            .broken-cards-null
+              | NULL
         if getBoard.archived
         if getBoard.archived
           i.fa.fa-archive
           i.fa.fa-archive
       li.result-card-context.result-card-context-separator
       li.result-card-context.result-card-context-separator
@@ -16,8 +20,12 @@ template(name="resultCard")
         = ' '
         = ' '
       li.result-card-context(title="{{_ 'swimlane'}}")
       li.result-card-context(title="{{_ 'swimlane'}}")
         .result-card-block-wrapper
         .result-card-block-wrapper
-          +viewer
-            = getSwimlane.title
+          if swimlaneId
+            +viewer
+              = getSwimlane.title
+          else
+            .broken-cards-null
+              | NULL
         if getSwimlane.archived
         if getSwimlane.archived
           i.fa.fa-archive
           i.fa.fa-archive
       li.result-card-context.result-card-context-separator
       li.result-card-context.result-card-context-separator
@@ -26,7 +34,11 @@ template(name="resultCard")
         = ' '
         = ' '
       li.result-card-context(title="{{_ 'list'}}")
       li.result-card-context(title="{{_ 'list'}}")
         .result-card-block-wrapper
         .result-card-block-wrapper
-          +viewer
-            = getList.title
+          if listId
+            +viewer
+              = getList.title
+          else
+            .broken-cards-null
+              | NULL
         if getList.archived
         if getList.archived
           i.fa.fa-archive
           i.fa.fa-archive

+ 12 - 36
client/components/main/brokenCards.jade

@@ -3,39 +3,15 @@ template(name="brokenCardsHeaderBar")
     | {{_ 'broken-cards'}}
     | {{_ 'broken-cards'}}
 
 
 template(name="brokenCards")
 template(name="brokenCards")
-  .wrapper
-    .broken-cards-wrapper
-      each card in brokenCardsList
-        .broken-cards-card-wrapper
-          .broken-cards-card-title
-            = card.title
-          ul.broken-cards-context-list
-            li.broken-cards-context(title="{{_ 'board'}}")
-              if card.boardId
-                +viewer
-                  = card.getBoard.title
-              else
-                .broken-cards-null
-                  | NULL
-            li.broken-cards-context.broken-cards-context-separator
-              = ' '
-              | {{_ 'context-separator'}}
-              = ' '
-            li.broken-cards-context(title="{{_ 'swimlane'}}")
-              if card.swimlaneId
-                +viewer
-                  = card.getSwimlane.title
-              else
-                .broken-cards-null
-                  | NULL
-            li.broken-cards-context
-              = ' '
-              | {{_ 'context-separator'}}
-              = ' '
-            li.broken-cards-context(title="{{_ 'list'}}")
-              if card.listId
-                +viewer
-                  = card.getList.title
-              else
-                .broken-cards-null
-                  | NULL
+  if currentUser
+    if searching.get
+      +spinner
+    else if hasResults.get
+      .global-search-results-list-wrapper
+        if hasQueryErrors.get
+          div
+            each msg in errorMessages
+              span.global-search-error-messages
+                = msg
+        else
+          +resultsPaged(this)

+ 8 - 18
client/components/main/brokenCards.js

@@ -1,3 +1,5 @@
+import { CardSearchPagedComponent } from '../../lib/cardSearch';
+
 BlazeComponent.extendComponent({}).register('brokenCardsHeaderBar');
 BlazeComponent.extendComponent({}).register('brokenCardsHeaderBar');
 
 
 Template.brokenCards.helpers({
 Template.brokenCards.helpers({
@@ -6,23 +8,11 @@ Template.brokenCards.helpers({
   },
   },
 });
 });
 
 
-BlazeComponent.extendComponent({
+class BrokenCardsComponent extends CardSearchPagedComponent {
   onCreated() {
   onCreated() {
-    Meteor.subscribe('setting');
-    Meteor.subscribe('brokenCards');
-  },
+    super.onCreated();
 
 
-  brokenCardsList() {
-    const selector = {
-      $or: [
-        { boardId: { $in: [null, ''] } },
-        { swimlaneId: { $in: [null, ''] } },
-        { listId: { $in: [null, ''] } },
-        { permission: 'public' },
-        { members: { $elemMatch: { userId: user._id, isActive: true } } },
-      ],
-    };
-
-    return Cards.find(selector);
-  },
-}).register('brokenCards');
+    Meteor.subscribe('brokenCards', this.sessionId);
+  }
+}
+BrokenCardsComponent.register('brokenCards');

+ 10 - 6
client/components/main/dueCards.jade

@@ -22,13 +22,17 @@ template(name="dueCardsModalTitle")
 
 
 template(name="dueCards")
 template(name="dueCards")
   if currentUser
   if currentUser
-    if isPageReady.get
-      .wrapper
-        .due-cards-dueat-list-wrapper
-          each card in dueCardsList
-            +resultCard(card)
-    else
+    if searching.get
       +spinner
       +spinner
+    else if hasResults.get
+      .global-search-results-list-wrapper
+        if hasQueryErrors.get
+          div
+            each msg in errorMessages
+              span.global-search-error-messages
+                = msg
+        else
+          +resultsPaged(this)
 
 
 template(name="dueCardsViewChangePopup")
 template(name="dueCardsViewChangePopup")
   if currentUser
   if currentUser

+ 46 - 89
client/components/main/dueCards.js

@@ -1,4 +1,14 @@
-const subManager = new SubsManager();
+import { CardSearchPagedComponent } from '../../lib/cardSearch';
+import {
+  OPERATOR_HAS,
+  OPERATOR_SORT,
+  OPERATOR_USER,
+  ORDER_DESCENDING,
+  PREDICATE_DUE_AT,
+} from '../../../config/search-const';
+import { QueryParams } from '../../../config/query-classes';
+
+// const subManager = new SubsManager();
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   dueCardsView() {
   dueCardsView() {
@@ -40,106 +50,51 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('dueCardsViewChangePopup');
 }).register('dueCardsViewChangePopup');
 
 
-BlazeComponent.extendComponent({
+class DueCardsComponent extends CardSearchPagedComponent {
   onCreated() {
   onCreated() {
-    this.isPageReady = new ReactiveVar(false);
-
-    this.autorun(() => {
-      const handle = subManager.subscribe(
-        'dueCards',
-        Utils.dueCardsView() === 'all',
-      );
-      Tracker.nonreactive(() => {
-        Tracker.autorun(() => {
-          this.isPageReady.set(handle.ready());
-        });
-      });
+    super.onCreated();
+
+    const queryParams = new QueryParams();
+    queryParams.addPredicate(OPERATOR_HAS, {
+      field: PREDICATE_DUE_AT,
+      exists: true,
     });
     });
-    Meteor.subscribe('setting');
-  },
+    // queryParams[OPERATOR_LIMIT] = 5;
+    queryParams.addPredicate(OPERATOR_SORT, {
+      name: PREDICATE_DUE_AT,
+      order: ORDER_DESCENDING,
+    });
+
+    if (Utils.dueCardsView() !== 'all') {
+      queryParams.addPredicate(OPERATOR_USER, Meteor.user().username);
+    }
+
+    this.runGlobalSearch(queryParams.getParams());
+  }
 
 
   dueCardsView() {
   dueCardsView() {
     // eslint-disable-next-line no-console
     // eslint-disable-next-line no-console
     //console.log('sort:', Utils.dueCardsView());
     //console.log('sort:', Utils.dueCardsView());
     return Utils.dueCardsView();
     return Utils.dueCardsView();
-  },
+  }
 
 
   sortByBoard() {
   sortByBoard() {
     return this.dueCardsView() === 'board';
     return this.dueCardsView() === 'board';
-  },
+  }
 
 
   dueCardsList() {
   dueCardsList() {
-    const allUsers = Utils.dueCardsView() === 'all';
-
-    const user = Meteor.user();
-
-    const archivedBoards = [];
-    Boards.find({ archived: true }).forEach(board => {
-      archivedBoards.push(board._id);
-    });
-
-    const permiitedBoards = [];
-    let selector = {
-      archived: false,
-    };
-    // for every user including admin allow her to see cards only from public boards
-    // or those 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);
-    });
-
-    const archivedSwimlanes = [];
-    Swimlanes.find({ archived: true }).forEach(swimlane => {
-      archivedSwimlanes.push(swimlane._id);
-    });
-
-    const archivedLists = [];
-    Lists.find({ archived: true }).forEach(list => {
-      archivedLists.push(list._id);
-    });
-
-    selector = {
-      archived: false,
-      boardId: {
-        $nin: archivedBoards,
-        $in: permiitedBoards,
-      },
-      swimlaneId: { $nin: archivedSwimlanes },
-      listId: { $nin: archivedLists },
-      dueAt: { $ne: null },
-      endAt: null,
-    };
-
-    if (!allUsers) {
-      selector.$or = [{ members: user._id }, { assignees: user._id }];
-    }
-
+    const results = this.getResults();
+    console.log('results:', results);
     const cards = [];
     const cards = [];
-
-    // eslint-disable-next-line no-console
-    // console.log('cards selector:', selector);
-    Cards.find(selector).forEach(card => {
-      cards.push(card);
-      // eslint-disable-next-line no-console
-      // console.log(
-      //   'board:',
-      //   card.board(),
-      //   'swimlane:',
-      //   card.swimlane(),
-      //   'list:',
-      //   card.list(),
-      // );
-    });
+    if (results) {
+      results.forEach(card => {
+        cards.push(card);
+      });
+    }
 
 
     cards.sort((a, b) => {
     cards.sort((a, b) => {
-      const x = a.dueAt === null ? Date('2100-12-31') : a.dueAt;
-      const y = b.dueAt === null ? Date('2100-12-31') : b.dueAt;
+      const x = a.dueAt === null ? new Date('2100-12-31') : a.dueAt;
+      const y = b.dueAt === null ? new Date('2100-12-31') : b.dueAt;
 
 
       if (x > y) return 1;
       if (x > y) return 1;
       else if (x < y) return -1;
       else if (x < y) return -1;
@@ -148,7 +103,9 @@ BlazeComponent.extendComponent({
     });
     });
 
 
     // eslint-disable-next-line no-console
     // eslint-disable-next-line no-console
-    // console.log('cards:', cards);
+    console.log('cards:', cards);
     return cards;
     return cards;
-  },
-}).register('dueCards');
+  }
+}
+
+DueCardsComponent.register('dueCards');

+ 28 - 38
client/components/main/globalSearch.jade

@@ -10,11 +10,29 @@ template(name="globalSearchModalTitle")
       i.fa.fa-keyboard-o
       i.fa.fa-keyboard-o
       | {{_ 'globalSearch-title'}}
       | {{_ 'globalSearch-title'}}
 
 
+template(name="resultsPaged")
+  h1
+    = resultsHeading.get
+    a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
+  each card in results.get
+    +resultCard(card)
+  table.global-search-footer
+    tr
+      td.global-search-previous-page
+        if hasPreviousPage.get
+          button.js-previous-page
+            | {{_ 'previous-page' }}
+      td.global-search-next-page(align="right")
+        if hasNextPage.get
+          button.js-next-page
+            | {{_ 'next-page' }}
+
 template(name="globalSearch")
 template(name="globalSearch")
   if currentUser
   if currentUser
     .wrapper
     .wrapper
       form.global-search-page.js-search-query-form
       form.global-search-page.js-search-query-form
         input.global-search-query-input(
         input.global-search-query-input(
+          style="{# if hasResults.get #}display: inline-block;{#/if#}"
           id="global-search-input"
           id="global-search-input"
           type="text"
           type="text"
           name="searchQuery"
           name="searchQuery"
@@ -22,31 +40,24 @@ template(name="globalSearch")
           value="{{ query.get }}"
           value="{{ query.get }}"
           autofocus dir="auto"
           autofocus dir="auto"
         )
         )
+        a.js-new-search.fa.fa-eraser
       if searching.get
       if searching.get
         +spinner
         +spinner
       else if hasResults.get
       else if hasResults.get
         .global-search-results-list-wrapper
         .global-search-results-list-wrapper
           if hasQueryErrors.get
           if hasQueryErrors.get
-            div
+            ul
               each msg in errorMessages
               each msg in errorMessages
-                span.global-search-error-messages
+                li.global-search-error-messages
                   = msg
                   = msg
           else
           else
-            h1
-              = resultsHeading.get
-              a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
-            each card in results.get
-              +resultCard(card)
-            table.global-search-footer
-              tr
-                td.global-search-previous-page
-                  if hasPreviousPage.get
-                    button.js-previous-page
-                      | {{_ 'previous-page' }}
-                td.global-search-next-page(align="right")
-                  if hasNextPage.get
-                    button.js-next-page
-                      | {{_ 'next-page' }}
+            +resultsPaged(this)
+      else if serverError.get
+        .global-search-page
+          .global-search-help
+            h1 {{_ 'server-error' }}
+            +viewer
+              | {{_ 'server-error-troubleshooting' }}
       else
       else
         .global-search-page
         .global-search-page
           .global-search-help
           .global-search-help
@@ -73,24 +84,3 @@ template(name="globalSearch")
           .global-search-instructions
           .global-search-instructions
             +viewer
             +viewer
               = searchInstructions
               = searchInstructions
-
-template(name="globalSearchViewChangePopup")
-  if currentUser
-    ul.pop-over-list
-      li
-        with "globalSearchViewChange-choice-me"
-          a.js-global-search-view-me
-            i.fa.fa-user.colorful
-            | {{_ 'globalSearchViewChange-choice-me'}}
-            if $eq Utils.globalSearchView "me"
-              i.fa.fa-check
-      li
-        with "globalSearchViewChange-choice-all"
-          a.js-global-search-view-all
-            i.fa.fa-users.colorful
-            | {{_ 'globalSearchViewChange-choice-all'}}
-            span.sub-name
-              +viewer
-                | {{_ 'globalSearchViewChange-choice-all-description' }}
-            if $eq Utils.globalSearchView "all"
-              i.fa.fa-check

+ 81 - 560
client/components/main/globalSearch.js

@@ -1,4 +1,8 @@
-const subManager = new SubsManager();
+import { CardSearchPagedComponent } from '../../lib/cardSearch';
+import Boards from '../../../models/boards';
+import { Query, QueryErrors } from '../../../config/query-classes';
+
+// const subManager = new SubsManager();
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   events() {
   events() {
@@ -16,45 +20,14 @@ Template.globalSearch.helpers({
   },
   },
 });
 });
 
 
-BlazeComponent.extendComponent({
-  events() {
-    return [
-      {
-        'click .js-due-cards-view-me'() {
-          Utils.setDueCardsView('me');
-          Popup.close();
-        },
-
-        'click .js-due-cards-view-all'() {
-          Utils.setDueCardsView('all');
-          Popup.close();
-        },
-      },
-    ];
-  },
-}).register('globalSearchViewChangePopup');
-
-BlazeComponent.extendComponent({
+class GlobalSearchComponent extends CardSearchPagedComponent {
   onCreated() {
   onCreated() {
-    this.searching = new ReactiveVar(false);
-    this.hasResults = new ReactiveVar(false);
-    this.hasQueryErrors = new ReactiveVar(false);
-    this.query = new ReactiveVar('');
-    this.resultsHeading = new ReactiveVar('');
-    this.searchLink = new ReactiveVar(null);
+    super.onCreated();
     this.myLists = new ReactiveVar([]);
     this.myLists = new ReactiveVar([]);
     this.myLabelNames = new ReactiveVar([]);
     this.myLabelNames = new ReactiveVar([]);
     this.myBoardNames = new ReactiveVar([]);
     this.myBoardNames = new ReactiveVar([]);
-    this.results = new ReactiveVar([]);
-    this.hasNextPage = new ReactiveVar(false);
-    this.hasPreviousPage = new ReactiveVar(false);
+    this.parsingErrors = new QueryErrors();
     this.queryParams = null;
     this.queryParams = null;
-    this.parsingErrors = [];
-    this.resultsCount = 0;
-    this.totalHits = 0;
-    this.queryErrors = null;
-    this.colorMap = null;
-    this.resultsPerPage = 25;
 
 
     Meteor.call('myLists', (err, data) => {
     Meteor.call('myLists', (err, data) => {
       if (!err) {
       if (!err) {
@@ -73,510 +46,71 @@ BlazeComponent.extendComponent({
         this.myBoardNames.set(data);
         this.myBoardNames.set(data);
       }
       }
     });
     });
-  },
+  }
 
 
   onRendered() {
   onRendered() {
     Meteor.subscribe('setting');
     Meteor.subscribe('setting');
 
 
     // eslint-disable-next-line no-console
     // eslint-disable-next-line no-console
     //console.log('lang:', TAPi18n.getLanguage());
     //console.log('lang:', TAPi18n.getLanguage());
-    this.colorMap = Boards.colorMap();
-    // eslint-disable-next-line no-console
-    // console.log('colorMap:', this.colorMap);
 
 
     if (Session.get('globalQuery')) {
     if (Session.get('globalQuery')) {
       this.searchAllBoards(Session.get('globalQuery'));
       this.searchAllBoards(Session.get('globalQuery'));
     }
     }
-  },
+  }
 
 
   resetSearch() {
   resetSearch() {
-    this.searching.set(false);
-    this.results.set([]);
-    this.hasResults.set(false);
-    this.hasQueryErrors.set(false);
-    this.resultsHeading.set('');
-    this.parsingErrors = [];
-    this.resultsCount = 0;
-    this.totalHits = 0;
-    this.queryErrors = null;
-  },
-
-  getSessionData() {
-    return SessionData.findOne({
-      userId: Meteor.userId(),
-      sessionId: SessionData.getSessionId(),
-    });
-  },
-
-  getResults() {
-    // eslint-disable-next-line no-console
-    // console.log('getting results');
-    if (this.queryParams) {
-      const sessionData = this.getSessionData();
-      // eslint-disable-next-line no-console
-      // console.log('selector:', sessionData.getSelector());
-      // console.log('session data:', sessionData);
-      const projection = sessionData.getProjection();
-      projection.skip = 0;
-      const cards = Cards.find({ _id: { $in: sessionData.cards } }, projection);
-      this.queryErrors = sessionData.errors;
-      if (this.queryErrors.length) {
-        this.hasQueryErrors.set(true);
-        return null;
-      }
-
-      if (cards) {
-        this.totalHits = sessionData.totalHits;
-        this.resultsCount = cards.count();
-        this.resultsStart = sessionData.lastHit - this.resultsCount + 1;
-        this.resultsEnd = sessionData.lastHit;
-        this.resultsHeading.set(this.getResultsHeading());
-        this.results.set(cards);
-        this.hasNextPage.set(sessionData.lastHit < sessionData.totalHits);
-        this.hasPreviousPage.set(
-          sessionData.lastHit - sessionData.resultsCount > 0,
-        );
-      }
-    }
-    this.resultsCount = 0;
-    return null;
-  },
+    super.resetSearch();
+    this.parsingErrors = new QueryErrors();
+  }
 
 
   errorMessages() {
   errorMessages() {
-    if (this.parsingErrors.length) {
-      return this.parsingErrorMessages();
+    if (this.parsingErrors.hasErrors()) {
+      return this.parsingErrors.errorMessages();
     }
     }
     return this.queryErrorMessages();
     return this.queryErrorMessages();
-  },
+  }
 
 
   parsingErrorMessages() {
   parsingErrorMessages() {
-    const messages = [];
-
-    if (this.parsingErrors.length) {
-      this.parsingErrors.forEach(err => {
-        messages.push(TAPi18n.__(err.tag, err.value));
-      });
-    }
+    this.parsingErrors.errorMessages();
+  }
 
 
-    return messages;
-  },
-
-  queryErrorMessages() {
-    messages = [];
-
-    this.queryErrors.forEach(err => {
-      let value = err.color ? TAPi18n.__(`color-${err.value}`) : err.value;
-      if (!value) {
-        value = err.value;
-      }
-      messages.push(TAPi18n.__(err.tag, value));
-    });
-
-    return messages;
-  },
-
-  searchAllBoards(query) {
-    query = query.trim();
+  searchAllBoards(queryText) {
+    queryText = queryText.trim();
     // eslint-disable-next-line no-console
     // eslint-disable-next-line no-console
-    //console.log('query:', query);
+    //console.log('queryText:', queryText);
 
 
-    this.query.set(query);
+    this.query.set(queryText);
 
 
     this.resetSearch();
     this.resetSearch();
 
 
-    if (!query) {
+    if (!queryText) {
       return;
       return;
     }
     }
 
 
     this.searching.set(true);
     this.searching.set(true);
 
 
-    const reOperator1 = new RegExp(
-      '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<value>[\\p{Letter}\\p{Mark}]+)(\\s+|$)',
-      'iu',
-    );
-    const reOperator2 = new RegExp(
-      '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<quote>["\']*)(?<value>.*?)\\k<quote>(\\s+|$)',
-      'iu',
-    );
-    const reText = new RegExp('^(?<text>\\S+)(\\s+|$)', 'u');
-    const reQuotedText = new RegExp(
-      '^(?<quote>["\'])(?<text>.*?)\\k<quote>(\\s+|$)',
-      'u',
-    );
-    const reNegatedOperator = new RegExp('^-(?<operator>.*)$');
-
-    const operators = {
-      'operator-board': 'boards',
-      'operator-board-abbrev': 'boards',
-      'operator-swimlane': 'swimlanes',
-      'operator-swimlane-abbrev': 'swimlanes',
-      'operator-list': 'lists',
-      'operator-list-abbrev': 'lists',
-      'operator-label': 'labels',
-      'operator-label-abbrev': 'labels',
-      'operator-user': 'users',
-      'operator-user-abbrev': 'users',
-      'operator-member': 'members',
-      'operator-member-abbrev': 'members',
-      'operator-assignee': 'assignees',
-      'operator-assignee-abbrev': 'assignees',
-      'operator-status': 'status',
-      'operator-due': 'dueAt',
-      'operator-created': 'createdAt',
-      'operator-modified': 'modifiedAt',
-      'operator-comment': 'comments',
-      'operator-has': 'has',
-      'operator-sort': 'sort',
-      'operator-limit': 'limit',
-    };
-
-    const predicates = {
-      due: {
-        'predicate-overdue': 'overdue',
-      },
-      durations: {
-        'predicate-week': 'week',
-        'predicate-month': 'month',
-        'predicate-quarter': 'quarter',
-        'predicate-year': 'year',
-      },
-      status: {
-        'predicate-archived': 'archived',
-        'predicate-all': 'all',
-        'predicate-open': 'open',
-        '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',
-        'predicate-start': 'startAt',
-        'predicate-end': 'endAt',
-        'predicate-due': 'dueAt',
-        'predicate-assignee': 'assignees',
-        'predicate-member': 'members',
-      },
-    };
-    const predicateTranslations = {};
-    Object.entries(predicates).forEach(([category, catPreds]) => {
-      predicateTranslations[category] = {};
-      Object.entries(catPreds).forEach(([tag, value]) => {
-        predicateTranslations[category][TAPi18n.__(tag)] = value;
-      });
-    });
-    // eslint-disable-next-line no-console
-    // console.log('predicateTranslations:', predicateTranslations);
-
-    const operatorMap = {};
-    Object.entries(operators).forEach(([key, value]) => {
-      operatorMap[TAPi18n.__(key).toLowerCase()] = value;
-    });
-    // eslint-disable-next-line no-console
-    // console.log('operatorMap:', operatorMap);
-
-    const params = {
-      limit: this.resultsPerPage,
-      boards: [],
-      swimlanes: [],
-      lists: [],
-      users: [],
-      members: [],
-      assignees: [],
-      labels: [],
-      status: [],
-      dueAt: null,
-      createdAt: null,
-      modifiedAt: null,
-      comments: [],
-      has: [],
-    };
-
-    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.toLowerCase();
-        }
-        // eslint-disable-next-line no-prototype-builtins
-        if (operatorMap.hasOwnProperty(op)) {
-          const operator = operatorMap[op];
-          let value = m.groups.value;
-          if (operator === 'labels') {
-            if (value in this.colorMap) {
-              value = this.colorMap[value];
-              // console.log('found color:', value);
-            }
-          } else if (['dueAt', 'createdAt', 'modifiedAt'].includes(operator)) {
-            let days = parseInt(value, 10);
-            let duration = null;
-            if (isNaN(days)) {
-              // duration was specified as text
-              if (predicateTranslations.durations[value]) {
-                duration = predicateTranslations.durations[value];
-                let date = null;
-                switch (duration) {
-                  case 'week':
-                    let week = moment().week();
-                    if (week === 52) {
-                      date = moment(1, 'W');
-                      date.set('year', date.year() + 1);
-                    } else {
-                      date = moment(week + 1, 'W');
-                    }
-                    break;
-                  case 'month':
-                    let month = moment().month();
-                    // .month() is zero indexed
-                    if (month === 11) {
-                      date = moment(1, 'M');
-                      date.set('year', date.year() + 1);
-                    } else {
-                      date = moment(month + 2, 'M');
-                    }
-                    break;
-                  case 'quarter':
-                    let quarter = moment().quarter();
-                    if (quarter === 4) {
-                      date = moment(1, 'Q');
-                      date.set('year', date.year() + 1);
-                    } else {
-                      date = moment(quarter + 1, 'Q');
-                    }
-                    break;
-                  case 'year':
-                    date = moment(moment().year() + 1, 'YYYY');
-                    break;
-                }
-                if (date) {
-                  value = {
-                    operator: '$lt',
-                    value: date.format('YYYY-MM-DD'),
-                  };
-                }
-              } else if (operator === 'dueAt' && value === 'overdue') {
-                value = {
-                  operator: '$lt',
-                  value: moment().format('YYYY-MM-DD'),
-                };
-              } else {
-                this.parsingErrors.push({
-                  tag: 'operator-number-expected',
-                  value: { operator: op, value },
-                });
-                value = null;
-              }
-            } else {
-              if (operator === 'dueAt') {
-                value = {
-                  operator: '$lt',
-                  value: moment(moment().format('YYYY-MM-DD'))
-                    .add(days + 1, duration ? duration : 'days')
-                    .format(),
-                };
-              } else {
-                value = {
-                  operator: '$gte',
-                  value: moment(moment().format('YYYY-MM-DD'))
-                    .subtract(days, duration ? duration : 'days')
-                    .format(),
-                };
-              }
-            }
-          } else if (operator === 'sort') {
-            let negated = false;
-            const m = value.match(reNegatedOperator);
-            if (m) {
-              value = m.groups.operator;
-              negated = true;
-            }
-            if (!predicateTranslations.sorts[value]) {
-              this.parsingErrors.push({
-                tag: 'operator-sort-invalid',
-                value,
-              });
-            } else {
-              value = {
-                name: predicateTranslations.sorts[value],
-                order: negated ? 'des' : 'asc',
-              };
-            }
-          } else if (operator === 'status') {
-            if (!predicateTranslations.status[value]) {
-              this.parsingErrors.push({
-                tag: 'operator-status-invalid',
-                value,
-              });
-            } else {
-              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 = {
-                field: predicateTranslations.has[value],
-                exists: !negated,
-              };
-            }
-          } else if (operator === 'limit') {
-            const limit = parseInt(value, 10);
-            if (isNaN(limit) || limit < 1) {
-              this.parsingErrors.push({
-                tag: 'operator-limit-invalid',
-                value,
-              });
-            } else {
-              value = limit;
-            }
-          }
-          if (Array.isArray(params[operator])) {
-            params[operator].push(value);
-          } else {
-            params[operator] = 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;
+    const query = new Query();
+    query.buildParams(queryText);
 
 
     // eslint-disable-next-line no-console
     // eslint-disable-next-line no-console
-    console.log('params:', params);
+    // console.log('params:', query.getParams());
 
 
-    this.queryParams = params;
+    this.queryParams = query.getParams();
 
 
-    if (this.parsingErrors.length) {
+    if (query.hasErrors()) {
       this.searching.set(false);
       this.searching.set(false);
-      this.queryErrors = this.parsingErrorMessages();
+      this.queryErrors = query.errors();
       this.hasResults.set(true);
       this.hasResults.set(true);
       this.hasQueryErrors.set(true);
       this.hasQueryErrors.set(true);
       return;
       return;
     }
     }
 
 
-    this.autorun(() => {
-      const handle = Meteor.subscribe(
-        'globalSearch',
-        SessionData.getSessionId(),
-        params,
-      );
-      Tracker.nonreactive(() => {
-        Tracker.autorun(() => {
-          if (handle.ready()) {
-            this.getResults();
-            this.searching.set(false);
-            this.hasResults.set(true);
-          }
-        });
-      });
-    });
-  },
-
-  nextPage() {
-    const sessionData = this.getSessionData();
-
-    this.autorun(() => {
-      const handle = Meteor.subscribe('nextPage', sessionData.sessionId);
-      Tracker.nonreactive(() => {
-        Tracker.autorun(() => {
-          if (handle.ready()) {
-            this.getResults();
-            this.searching.set(false);
-            this.hasResults.set(true);
-          }
-        });
-      });
-    });
-  },
-
-  previousPage() {
-    const sessionData = this.getSessionData();
-
-    this.autorun(() => {
-      const handle = Meteor.subscribe('previousPage', sessionData.sessionId);
-      Tracker.nonreactive(() => {
-        Tracker.autorun(() => {
-          if (handle.ready()) {
-            this.getResults();
-            this.searching.set(false);
-            this.hasResults.set(true);
-          }
-        });
-      });
-    });
-  },
-
-  getResultsHeading() {
-    if (this.resultsCount === 0) {
-      return TAPi18n.__('no-cards-found');
-    } else if (this.resultsCount === 1) {
-      return TAPi18n.__('one-card-found');
-    } else if (this.resultsCount === this.totalHits) {
-      return TAPi18n.__('n-cards-found', this.resultsCount);
-    }
-
-    return TAPi18n.__('n-n-of-n-cards-found', {
-      start: this.resultsStart,
-      end: this.resultsEnd,
-      total: this.totalHits,
-    });
-  },
-
-  getSearchHref() {
-    const baseUrl = window.location.href.replace(/([?#].*$|\s*$)/, '');
-    return `${baseUrl}?q=${encodeURIComponent(this.query.get())}`;
-  },
+    this.runGlobalSearch(query.getParams());
+  }
 
 
   searchInstructions() {
   searchInstructions() {
-    tags = {
+    const tags = {
       operator_board: TAPi18n.__('operator-board'),
       operator_board: TAPi18n.__('operator-board'),
       operator_list: TAPi18n.__('operator-list'),
       operator_list: TAPi18n.__('operator-list'),
       operator_swimlane: TAPi18n.__('operator-swimlane'),
       operator_swimlane: TAPi18n.__('operator-swimlane'),
@@ -618,61 +152,46 @@ BlazeComponent.extendComponent({
       predicate_member: TAPi18n.__('predicate-member'),
       predicate_member: TAPi18n.__('predicate-member'),
     };
     };
 
 
-    let text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`;
-    text += `\n${TAPi18n.__('globalSearch-instructions-description', tags)}`;
-    text += `\n\n${TAPi18n.__('globalSearch-instructions-operators', tags)}`;
-
-    [
-      'globalSearch-instructions-operator-board',
-      'globalSearch-instructions-operator-list',
-      'globalSearch-instructions-operator-swimlane',
-      'globalSearch-instructions-operator-comment',
-      'globalSearch-instructions-operator-label',
-      'globalSearch-instructions-operator-hash',
-      'globalSearch-instructions-operator-user',
-      'globalSearch-instructions-operator-at',
-      'globalSearch-instructions-operator-member',
-      'globalSearch-instructions-operator-assignee',
-      'globalSearch-instructions-operator-due',
-      'globalSearch-instructions-operator-created',
-      'globalSearch-instructions-operator-modified',
-      'globalSearch-instructions-operator-status',
-    ].forEach(instruction => {
-      text += `\n* ${TAPi18n.__(instruction, tags)}`;
-    });
-
-    [
-      'globalSearch-instructions-status-archived',
-      'globalSearch-instructions-status-public',
-      'globalSearch-instructions-status-private',
-      'globalSearch-instructions-status-all',
-      'globalSearch-instructions-status-ended',
-    ].forEach(instruction => {
-      text += `\n    * ${TAPi18n.__(instruction, tags)}`;
-    });
-
-    [
-      'globalSearch-instructions-operator-has',
-      'globalSearch-instructions-operator-sort',
-      'globalSearch-instructions-operator-limit',
-    ].forEach(instruction => {
-      text += `\n* ${TAPi18n.__(instruction, tags)}`;
-    });
-
-    text += `\n## ${TAPi18n.__('heading-notes')}`;
+    let text = '';
     [
     [
-      'globalSearch-instructions-notes-1',
-      'globalSearch-instructions-notes-2',
-      'globalSearch-instructions-notes-3',
-      'globalSearch-instructions-notes-3-2',
-      'globalSearch-instructions-notes-4',
-      'globalSearch-instructions-notes-5',
-    ].forEach(instruction => {
-      text += `\n* ${TAPi18n.__(instruction, tags)}`;
+      ['# ', 'globalSearch-instructions-heading'],
+      ['\n', 'globalSearch-instructions-description'],
+      ['\n\n', 'globalSearch-instructions-operators'],
+      ['\n* ', 'globalSearch-instructions-operator-board'],
+      ['\n* ', 'globalSearch-instructions-operator-list'],
+      ['\n* ', 'globalSearch-instructions-operator-swimlane'],
+      ['\n* ', 'globalSearch-instructions-operator-comment'],
+      ['\n* ', 'globalSearch-instructions-operator-label'],
+      ['\n* ', 'globalSearch-instructions-operator-hash'],
+      ['\n* ', 'globalSearch-instructions-operator-user'],
+      ['\n* ', 'globalSearch-instructions-operator-at'],
+      ['\n* ', 'globalSearch-instructions-operator-member'],
+      ['\n* ', 'globalSearch-instructions-operator-assignee'],
+      ['\n* ', 'globalSearch-instructions-operator-due'],
+      ['\n* ', 'globalSearch-instructions-operator-created'],
+      ['\n* ', 'globalSearch-instructions-operator-modified'],
+      ['\n* ', 'globalSearch-instructions-operator-status'],
+      ['\n    * ', 'globalSearch-instructions-status-archived'],
+      ['\n    * ', 'globalSearch-instructions-status-public'],
+      ['\n    * ', 'globalSearch-instructions-status-private'],
+      ['\n    * ', 'globalSearch-instructions-status-all'],
+      ['\n    * ', 'globalSearch-instructions-status-ended'],
+      ['\n* ', 'globalSearch-instructions-operator-has'],
+      ['\n* ', 'globalSearch-instructions-operator-sort'],
+      ['\n* ', 'globalSearch-instructions-operator-limit'],
+      ['\n## ', 'heading-notes'],
+      ['\n* ', 'globalSearch-instructions-notes-1'],
+      ['\n* ', 'globalSearch-instructions-notes-2'],
+      ['\n* ', 'globalSearch-instructions-notes-3'],
+      ['\n* ', 'globalSearch-instructions-notes-3-2'],
+      ['\n* ', 'globalSearch-instructions-notes-4'],
+      ['\n* ', 'globalSearch-instructions-notes-5'],
+    ].forEach(([prefix, instruction]) => {
+      text += `${prefix}${TAPi18n.__(instruction, tags)}`;
     });
     });
 
 
     return text;
     return text;
-  },
+  }
 
 
   labelColors() {
   labelColors() {
     return Boards.simpleSchema()._schema['labels.$.color'].allowedValues.map(
     return Boards.simpleSchema()._schema['labels.$.color'].allowedValues.map(
@@ -680,23 +199,16 @@ BlazeComponent.extendComponent({
         return { color, name: TAPi18n.__(`color-${color}`) };
         return { color, name: TAPi18n.__(`color-${color}`) };
       },
       },
     );
     );
-  },
+  }
 
 
   events() {
   events() {
     return [
     return [
       {
       {
+        ...super.events()[0],
         'submit .js-search-query-form'(evt) {
         'submit .js-search-query-form'(evt) {
           evt.preventDefault();
           evt.preventDefault();
           this.searchAllBoards(evt.target.searchQuery.value);
           this.searchAllBoards(evt.target.searchQuery.value);
         },
         },
-        'click .js-next-page'(evt) {
-          evt.preventDefault();
-          this.nextPage();
-        },
-        'click .js-previous-page'(evt) {
-          evt.preventDefault();
-          this.previousPage();
-        },
         'click .js-label-color'(evt) {
         'click .js-label-color'(evt) {
           evt.preventDefault();
           evt.preventDefault();
           const input = document.getElementById('global-search-input');
           const input = document.getElementById('global-search-input');
@@ -737,7 +249,16 @@ BlazeComponent.extendComponent({
           );
           );
           document.getElementById('global-search-input').focus();
           document.getElementById('global-search-input').focus();
         },
         },
+        'click .js-new-search'(evt) {
+          evt.preventDefault();
+          const input = document.getElementById('global-search-input');
+          input.value = '';
+          this.query.set('');
+          this.hasResults.set(false);
+        },
       },
       },
     ];
     ];
-  },
-}).register('globalSearch');
+  }
+}
+
+GlobalSearchComponent.register('globalSearch');

+ 3 - 3
client/components/main/myCards.jade

@@ -24,7 +24,9 @@ template(name="myCardsModalTitle")
 
 
 template(name="myCards")
 template(name="myCards")
   if currentUser
   if currentUser
-    if isPageReady.get
+    if searching.get
+      +spinner
+    else
       .wrapper
       .wrapper
         if $eq myCardsSort 'board'
         if $eq myCardsSort 'board'
           each board in myCardsList
           each board in myCardsList
@@ -50,8 +52,6 @@ template(name="myCards")
           .my-cards-dueat-list-wrapper
           .my-cards-dueat-list-wrapper
             each card in myDueCardsList
             each card in myDueCardsList
               +resultCard(card)
               +resultCard(card)
-    else
-      +spinner
 
 
 template(name="myCardsSortChangePopup")
 template(name="myCardsSortChangePopup")
   if currentUser
   if currentUser

+ 113 - 155
client/components/main/myCards.js

@@ -1,4 +1,14 @@
-const subManager = new SubsManager();
+import { CardSearchPagedComponent } from '../../lib/cardSearch';
+import { QueryParams } from '../../../config/query-classes';
+import {
+  OPERATOR_LIMIT,
+  OPERATOR_SORT,
+  OPERATOR_USER,
+  ORDER_DESCENDING,
+  PREDICATE_DUE_AT,
+} from '../../../config/search-const';
+
+// const subManager = new SubsManager();
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   myCardsSort() {
   myCardsSort() {
@@ -42,182 +52,147 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('myCardsSortChangePopup');
 }).register('myCardsSortChangePopup');
 
 
-BlazeComponent.extendComponent({
+class MyCardsComponent extends CardSearchPagedComponent {
   onCreated() {
   onCreated() {
-    this.isPageReady = new ReactiveVar(false);
+    super.onCreated();
 
 
-    this.autorun(() => {
-      const handle = subManager.subscribe('myCards');
-      Tracker.nonreactive(() => {
-        Tracker.autorun(() => {
-          this.isPageReady.set(handle.ready());
-        });
-      });
+    const queryParams = new QueryParams();
+    queryParams.addPredicate(OPERATOR_USER, Meteor.user().username);
+    queryParams.addPredicate(OPERATOR_SORT, {
+      name: PREDICATE_DUE_AT,
+      order: ORDER_DESCENDING,
     });
     });
+    queryParams.addPredicate(OPERATOR_LIMIT, 100);
+
+    this.runGlobalSearch(queryParams);
     Meteor.subscribe('setting');
     Meteor.subscribe('setting');
-  },
+  }
 
 
   myCardsSort() {
   myCardsSort() {
     // eslint-disable-next-line no-console
     // eslint-disable-next-line no-console
     //console.log('sort:', Utils.myCardsSort());
     //console.log('sort:', Utils.myCardsSort());
     return Utils.myCardsSort();
     return Utils.myCardsSort();
-  },
+  }
 
 
   sortByBoard() {
   sortByBoard() {
     return this.myCardsSort() === 'board';
     return this.myCardsSort() === 'board';
-  },
+  }
 
 
   myCardsList() {
   myCardsList() {
-    const userId = Meteor.userId();
     const boards = [];
     const boards = [];
     let board = null;
     let board = null;
     let swimlane = null;
     let swimlane = null;
     let list = null;
     let list = null;
 
 
-    const cursor = Cards.find(
-      {
-        $or: [{ members: userId }, { assignees: userId }],
-        archived: false,
-      },
-      {
-        sort: {
-          boardId: 1,
-          swimlaneId: 1,
-          listId: 1,
-          sort: 1,
-        },
-      },
-    );
+    const cursor = this.getResults();
 
 
-    let newBoard = false;
-    let newSwimlane = false;
-    let newList = false;
+    if (cursor) {
+      let newBoard = false;
+      let newSwimlane = false;
+      let newList = false;
 
 
-    cursor.forEach(card => {
-      // eslint-disable-next-line no-console
-      // console.log('card:', card.title);
-      if (list === null || card.listId !== list._id) {
+      cursor.forEach(card => {
         // eslint-disable-next-line no-console
         // eslint-disable-next-line no-console
-        // console.log('new list');
-        list = card.getList();
-        if (list.archived) {
-          list = null;
-          return;
+        // console.log('card:', card.title);
+        if (list === null || card.listId !== list._id) {
+          // eslint-disable-next-line no-console
+          // console.log('new list');
+          list = card.getList();
+          if (list.archived) {
+            list = null;
+            return;
+          }
+          list.myCards = [card];
+          newList = true;
         }
         }
-        list.myCards = [card];
-        newList = true;
-      }
-      if (swimlane === null || card.swimlaneId !== swimlane._id) {
-        // eslint-disable-next-line no-console
-        // console.log('new swimlane');
-        swimlane = card.getSwimlane();
-        if (swimlane.archived) {
-          swimlane = null;
-          return;
+        if (swimlane === null || card.swimlaneId !== swimlane._id) {
+          // eslint-disable-next-line no-console
+          // console.log('new swimlane');
+          swimlane = card.getSwimlane();
+          if (swimlane.archived) {
+            swimlane = null;
+            return;
+          }
+          swimlane.myLists = [list];
+          newSwimlane = true;
         }
         }
-        swimlane.myLists = [list];
-        newSwimlane = true;
-      }
-      if (board === null || card.boardId !== board._id) {
-        // eslint-disable-next-line no-console
-        // console.log('new board');
-        board = card.getBoard();
-        if (board.archived) {
-          board = null;
-          return;
+        if (board === null || card.boardId !== board._id) {
+          // eslint-disable-next-line no-console
+          // console.log('new board');
+          board = card.getBoard();
+          if (board.archived) {
+            board = null;
+            return;
+          }
+          // eslint-disable-next-line no-console
+          // console.log('board:', b, b._id, b.title);
+          board.mySwimlanes = [swimlane];
+          newBoard = true;
         }
         }
-        // eslint-disable-next-line no-console
-        // console.log('board:', b, b._id, b.title);
-        board.mySwimlanes = [swimlane];
-        newBoard = true;
-      }
-
-      if (newBoard) {
-        boards.push(board);
-      } else if (newSwimlane) {
-        board.mySwimlanes.push(swimlane);
-      } else if (newList) {
-        swimlane.myLists.push(list);
-      } else {
-        list.myCards.push(card);
-      }
-
-      newBoard = false;
-      newSwimlane = false;
-      newList = false;
-    });
 
 
-    // sort the data structure
-    boards.forEach(board => {
-      board.mySwimlanes.forEach(swimlane => {
-        swimlane.myLists.forEach(list => {
-          list.myCards.sort((a, b) => {
+        if (newBoard) {
+          boards.push(board);
+        } else if (newSwimlane) {
+          board.mySwimlanes.push(swimlane);
+        } else if (newList) {
+          swimlane.myLists.push(list);
+        } else {
+          list.myCards.push(card);
+        }
+
+        newBoard = false;
+        newSwimlane = false;
+        newList = false;
+      });
+
+      // sort the data structure
+      boards.forEach(board => {
+        board.mySwimlanes.forEach(swimlane => {
+          swimlane.myLists.forEach(list => {
+            list.myCards.sort((a, b) => {
+              return a.sort - b.sort;
+            });
+          });
+          swimlane.myLists.sort((a, b) => {
             return a.sort - b.sort;
             return a.sort - b.sort;
           });
           });
         });
         });
-        swimlane.myLists.sort((a, b) => {
+        board.mySwimlanes.sort((a, b) => {
           return a.sort - b.sort;
           return a.sort - b.sort;
         });
         });
       });
       });
-      board.mySwimlanes.sort((a, b) => {
-        return a.sort - b.sort;
-      });
-    });
-
-    boards.sort((a, b) => {
-      let x = a.sort;
-      let y = b.sort;
-
-      // show the template board last
-      if (a.type === 'template-container') {
-        x = 99999999;
-      } else if (b.type === 'template-container') {
-        y = 99999999;
-      }
-      return x - y;
-    });
 
 
-    // eslint-disable-next-line no-console
-    // console.log('boards:', boards);
-    return boards;
-  },
+      boards.sort((a, b) => {
+        let x = a.sort;
+        let y = b.sort;
 
 
-  myDueCardsList() {
-    const userId = Meteor.userId();
+        // show the template board last
+        if (a.type === 'template-container') {
+          x = 99999999;
+        } else if (b.type === 'template-container') {
+          y = 99999999;
+        }
+        return x - y;
+      });
 
 
-    const cursor = Cards.find(
-      {
-        $or: [{ members: userId }, { assignees: userId }],
-        archived: false,
-      },
-      {
-        sort: {
-          dueAt: -1,
-          boardId: 1,
-          swimlaneId: 1,
-          listId: 1,
-          sort: 1,
-        },
-      },
-    );
+      // eslint-disable-next-line no-console
+      // console.log('boards:', boards);
+      return boards;
+    }
 
 
-    // eslint-disable-next-line no-console
-    // console.log('cursor:', cursor);
+    return [];
+  }
 
 
+  myDueCardsList() {
+    const cursor = this.getResults();
     const cards = [];
     const cards = [];
     cursor.forEach(card => {
     cursor.forEach(card => {
-      if (
-        !card.getBoard().archived &&
-        !card.getSwimlane().archived &&
-        !card.getList().archived
-      ) {
-        cards.push(card);
-      }
+      cards.push(card);
     });
     });
 
 
     cards.sort((a, b) => {
     cards.sort((a, b) => {
-      const x = a.dueAt === null ? Date('2100-12-31') : a.dueAt;
-      const y = b.dueAt === null ? Date('2100-12-31') : b.dueAt;
+      const x = a.dueAt === null ? new Date('2100-12-31') : a.dueAt;
+      const y = b.dueAt === null ? new Date('2100-12-31') : b.dueAt;
 
 
       if (x > y) return 1;
       if (x > y) return 1;
       else if (x < y) return -1;
       else if (x < y) return -1;
@@ -228,23 +203,6 @@ BlazeComponent.extendComponent({
     // eslint-disable-next-line no-console
     // eslint-disable-next-line no-console
     // console.log('cursor:', cards);
     // console.log('cursor:', cards);
     return cards;
     return cards;
-  },
-
-  events() {
-    return [
-      {
-        // 'click .js-my-card'(evt) {
-        //   const card = this.currentData().card;
-        //   // eslint-disable-next-line no-console
-        //   console.log('currentData():', this.currentData());
-        //   // eslint-disable-next-line no-console
-        //   console.log('card:', card);
-        //   if (card) {
-        //     Utils.goCardId(card._id);
-        //   }
-        //   evt.preventDefault();
-        // },
-      },
-    ];
-  },
-}).register('myCards');
+  }
+}
+MyCardsComponent.register('myCards');

+ 182 - 0
client/lib/cardSearch.js

@@ -0,0 +1,182 @@
+import Cards from '../../models/cards';
+import SessionData from '../../models/usersessiondata';
+
+export class CardSearchPagedComponent extends BlazeComponent {
+  onCreated() {
+    this.searching = new ReactiveVar(false);
+    this.hasResults = new ReactiveVar(false);
+    this.hasQueryErrors = new ReactiveVar(false);
+    this.query = new ReactiveVar('');
+    this.resultsHeading = new ReactiveVar('');
+    this.searchLink = new ReactiveVar(null);
+    this.results = new ReactiveVar([]);
+    this.hasNextPage = new ReactiveVar(false);
+    this.hasPreviousPage = new ReactiveVar(false);
+    this.resultsCount = 0;
+    this.totalHits = 0;
+    this.queryErrors = null;
+    this.resultsPerPage = 25;
+    this.sessionId = SessionData.getSessionId();
+    this.subscriptionHandle = null;
+    this.serverError = new ReactiveVar(false);
+
+    const that = this;
+    this.subscriptionCallbacks = {
+      onReady() {
+        that.getResults();
+        that.searching.set(false);
+        that.hasResults.set(true);
+        that.serverError.set(false);
+      },
+      onError(error) {
+        that.searching.set(false);
+        that.hasResults.set(false);
+        that.serverError.set(true);
+        console.log('Error.reason:', error.reason);
+        console.log('Error.message:', error.message);
+        console.log('Error.stack:', error.stack);
+      },
+    };
+  }
+
+  resetSearch() {
+    this.searching.set(false);
+    this.results.set([]);
+    this.hasResults.set(false);
+    this.hasQueryErrors.set(false);
+    this.resultsHeading.set('');
+    this.serverError.set(false);
+    this.resultsCount = 0;
+    this.totalHits = 0;
+    this.queryErrors = null;
+  }
+
+  getSessionData(sessionId) {
+    return SessionData.findOne({
+      sessionId: sessionId ? sessionId : SessionData.getSessionId(),
+    });
+  }
+
+  getResults() {
+    // eslint-disable-next-line no-console
+    // console.log('getting results');
+    const sessionData = this.getSessionData();
+    // eslint-disable-next-line no-console
+    // console.log('selector:', sessionData.getSelector());
+    console.log('session data:', sessionData);
+    const cards = [];
+    sessionData.cards.forEach(cardId => {
+      cards.push(Cards.findOne({ _id: cardId }));
+    });
+    this.queryErrors = sessionData.errors;
+    if (this.queryErrors.length) {
+      // console.log('queryErrors:', this.queryErrorMessages());
+      this.hasQueryErrors.set(true);
+      return null;
+    }
+
+    if (cards) {
+      this.totalHits = sessionData.totalHits;
+      this.resultsCount = cards.length;
+      this.resultsStart = sessionData.lastHit - this.resultsCount + 1;
+      this.resultsEnd = sessionData.lastHit;
+      this.resultsHeading.set(this.getResultsHeading());
+      this.results.set(cards);
+      this.hasNextPage.set(sessionData.lastHit < sessionData.totalHits);
+      this.hasPreviousPage.set(
+        sessionData.lastHit - sessionData.resultsCount > 0,
+      );
+      return cards;
+    }
+
+    this.resultsCount = 0;
+    return null;
+  }
+
+  stopSubscription() {
+    if (this.subscriptionHandle) {
+      this.subscriptionHandle.stop();
+    }
+  }
+
+  runGlobalSearch(params) {
+    this.searching.set(true);
+    this.stopSubscription();
+    this.subscriptionHandle = Meteor.subscribe(
+      'globalSearch',
+      this.sessionId,
+      params,
+      this.subscriptionCallbacks,
+    );
+  }
+
+  queryErrorMessages() {
+    const messages = [];
+
+    this.queryErrors.forEach(err => {
+      let value = err.color ? TAPi18n.__(`color-${err.value}`) : err.value;
+      if (!value) {
+        value = err.value;
+      }
+      messages.push(TAPi18n.__(err.tag, value));
+    });
+
+    return messages;
+  }
+
+  nextPage() {
+    this.searching.set(true);
+    this.stopSubscription();
+    this.subscriptionHandle = Meteor.subscribe(
+      'nextPage',
+      this.sessionId,
+      this.subscriptionCallbacks,
+    );
+  }
+
+  previousPage() {
+    this.searching.set(true);
+    this.stopSubscription();
+    this.subscriptionHandle = Meteor.subscribe(
+      'previousPage',
+      this.sessionId,
+      this.subscriptionCallbacks,
+    );
+  }
+
+  getResultsHeading() {
+    if (this.resultsCount === 0) {
+      return TAPi18n.__('no-cards-found');
+    } else if (this.resultsCount === 1) {
+      return TAPi18n.__('one-card-found');
+    } else if (this.resultsCount === this.totalHits) {
+      return TAPi18n.__('n-cards-found', this.resultsCount);
+    }
+
+    return TAPi18n.__('n-n-of-n-cards-found', {
+      start: this.resultsStart,
+      end: this.resultsEnd,
+      total: this.totalHits,
+    });
+  }
+
+  getSearchHref() {
+    const baseUrl = window.location.href.replace(/([?#].*$|\s*$)/, '');
+    return `${baseUrl}?q=${encodeURIComponent(this.query.get())}`;
+  }
+
+  events() {
+    return [
+      {
+        'click .js-next-page'(evt) {
+          evt.preventDefault();
+          this.nextPage();
+        },
+        'click .js-previous-page'(evt) {
+          evt.preventDefault();
+          this.previousPage();
+        },
+      },
+    ];
+  }
+}

+ 503 - 0
config/query-classes.js

@@ -0,0 +1,503 @@
+import {
+  OPERATOR_ASSIGNEE,
+  OPERATOR_BOARD,
+  OPERATOR_COMMENT,
+  OPERATOR_CREATED_AT,
+  OPERATOR_DUE,
+  OPERATOR_HAS,
+  OPERATOR_LABEL,
+  OPERATOR_LIMIT,
+  OPERATOR_LIST,
+  OPERATOR_MEMBER,
+  OPERATOR_MODIFIED_AT,
+  OPERATOR_SORT,
+  OPERATOR_STATUS,
+  OPERATOR_SWIMLANE,
+  OPERATOR_UNKNOWN,
+  OPERATOR_USER,
+  ORDER_ASCENDING,
+  ORDER_DESCENDING,
+  PREDICATE_ALL,
+  PREDICATE_ARCHIVED,
+  PREDICATE_ASSIGNEES,
+  PREDICATE_ATTACHMENT,
+  PREDICATE_CHECKLIST,
+  PREDICATE_CREATED_AT,
+  PREDICATE_DESCRIPTION,
+  PREDICATE_DUE_AT,
+  PREDICATE_END_AT,
+  PREDICATE_ENDED,
+  PREDICATE_MEMBERS,
+  PREDICATE_MODIFIED_AT,
+  PREDICATE_MONTH,
+  PREDICATE_OPEN,
+  PREDICATE_OVERDUE,
+  PREDICATE_PRIVATE,
+  PREDICATE_PUBLIC,
+  PREDICATE_QUARTER,
+  PREDICATE_START_AT,
+  PREDICATE_WEEK,
+  PREDICATE_YEAR,
+} from './search-const';
+import Boards from '../models/boards';
+import moment from 'moment';
+
+export class QueryParams {
+  text = '';
+
+  constructor(params = {}) {
+    this.params = params;
+  }
+
+  hasOperator(operator) {
+    return this.params[operator];
+  }
+
+  addPredicate(operator, predicate) {
+    if (!this.hasOperator(operator)) {
+      this.params[operator] = [];
+    }
+    this.params[operator].push(predicate);
+  }
+
+  setPredicate(operator, predicate) {
+    this.params[operator] = predicate;
+  }
+
+  getPredicate(operator) {
+    return this.params[operator][0];
+  }
+
+  getPredicates(operator) {
+    return this.params[operator];
+  }
+
+  getParams() {
+    return this.params;
+  }
+}
+
+export class QueryErrors {
+  operatorTagMap = [
+    [OPERATOR_BOARD, 'board-title-not-found'],
+    [OPERATOR_SWIMLANE, 'swimlane-title-not-found'],
+    [
+      OPERATOR_LABEL,
+      label => {
+        if (Boards.labelColors().includes(label)) {
+          return {
+            tag: 'label-color-not-found',
+            value: label,
+            color: true,
+          };
+        } else {
+          return {
+            tag: 'label-not-found',
+            value: label,
+            color: false,
+          };
+        }
+      },
+    ],
+    [OPERATOR_LIST, 'list-title-not-found'],
+    [OPERATOR_COMMENT, 'comment-not-found'],
+    [OPERATOR_USER, 'user-username-not-found'],
+    [OPERATOR_ASSIGNEE, 'user-username-not-found'],
+    [OPERATOR_MEMBER, 'user-username-not-found'],
+  ];
+
+  constructor() {
+    this._errors = {};
+
+    this.operatorTags = {};
+    this.operatorTagMap.forEach(([operator, tag]) => {
+      this.operatorTags[operator] = tag;
+    });
+
+    this.colorMap = Boards.colorMap();
+  }
+
+  addError(operator, error) {
+    if (!this._errors[operator]) {
+      this._errors[operator] = [];
+    }
+    this._errors[operator].push(error);
+  }
+
+  addNotFound(operator, value) {
+    if (typeof this.operatorTags[operator] === 'function') {
+      this.addError(operator, this.operatorTags[operator](value));
+    } else {
+      this.addError(operator, { tag: this.operatorTags[operator], value });
+    }
+  }
+
+  hasErrors() {
+    return Object.entries(this._errors).length > 0;
+  }
+
+  errors() {
+    const errs = [];
+    // eslint-disable-next-line no-unused-vars
+    Object.entries(this._errors).forEach(([, errors]) => {
+      errors.forEach(err => {
+        errs.push(err);
+      });
+    });
+    return errs;
+  }
+
+  errorMessages() {
+    const messages = [];
+    // eslint-disable-next-line no-unused-vars
+    Object.entries(this._errors).forEach(([, errors]) => {
+      errors.forEach(err => {
+        messages.push(TAPi18n.__(err.tag, err.value));
+      });
+    });
+    return messages;
+  }
+}
+
+export class Query {
+  selector = {};
+  projection = {};
+
+  constructor(selector, projection) {
+    this._errors = new QueryErrors();
+    this.queryParams = new QueryParams();
+    this.colorMap = Boards.colorMap();
+
+    if (selector) {
+      this.selector = selector;
+    }
+
+    if (projection) {
+      this.projection = projection;
+    }
+  }
+
+  hasErrors() {
+    return this._errors.hasErrors();
+  }
+
+  errors() {
+    return this._errors.errors();
+  }
+
+  errorMessages() {
+    return this._errors.errorMessages();
+  }
+
+  getParams() {
+    return this.queryParams.getParams();
+  }
+
+  addPredicate(operator, predicate) {
+    this.queryParams.addPredicate(operator, predicate);
+  }
+
+  buildParams(queryText) {
+    queryText = queryText.trim();
+    // eslint-disable-next-line no-console
+    //console.log('query:', query);
+
+    if (!queryText) {
+      return;
+    }
+
+    const reOperator1 = new RegExp(
+      '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<value>[\\p{Letter}\\p{Mark}]+)(\\s+|$)',
+      'iu',
+    );
+    const reOperator2 = new RegExp(
+      '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<quote>["\']*)(?<value>.*?)\\k<quote>(\\s+|$)',
+      'iu',
+    );
+    const reText = new RegExp('^(?<text>\\S+)(\\s+|$)', 'u');
+    const reQuotedText = new RegExp(
+      '^(?<quote>["\'])(?<text>.*?)\\k<quote>(\\s+|$)',
+      'u',
+    );
+    const reNegatedOperator = new RegExp('^-(?<operator>.*)$');
+
+    const operators = {
+      'operator-board': OPERATOR_BOARD,
+      'operator-board-abbrev': OPERATOR_BOARD,
+      'operator-swimlane': OPERATOR_SWIMLANE,
+      'operator-swimlane-abbrev': OPERATOR_SWIMLANE,
+      'operator-list': OPERATOR_LIST,
+      'operator-list-abbrev': OPERATOR_LIST,
+      'operator-label': OPERATOR_LABEL,
+      'operator-label-abbrev': OPERATOR_LABEL,
+      'operator-user': OPERATOR_USER,
+      'operator-user-abbrev': OPERATOR_USER,
+      'operator-member': OPERATOR_MEMBER,
+      'operator-member-abbrev': OPERATOR_MEMBER,
+      'operator-assignee': OPERATOR_ASSIGNEE,
+      'operator-assignee-abbrev': OPERATOR_ASSIGNEE,
+      'operator-status': OPERATOR_STATUS,
+      'operator-due': OPERATOR_DUE,
+      'operator-created': OPERATOR_CREATED_AT,
+      'operator-modified': OPERATOR_MODIFIED_AT,
+      'operator-comment': OPERATOR_COMMENT,
+      'operator-has': OPERATOR_HAS,
+      'operator-sort': OPERATOR_SORT,
+      'operator-limit': OPERATOR_LIMIT,
+    };
+
+    const predicates = {
+      due: {
+        'predicate-overdue': PREDICATE_OVERDUE,
+      },
+      durations: {
+        'predicate-week': PREDICATE_WEEK,
+        'predicate-month': PREDICATE_MONTH,
+        'predicate-quarter': PREDICATE_QUARTER,
+        'predicate-year': PREDICATE_YEAR,
+      },
+      status: {
+        'predicate-archived': PREDICATE_ARCHIVED,
+        'predicate-all': PREDICATE_ALL,
+        'predicate-open': PREDICATE_OPEN,
+        'predicate-ended': PREDICATE_ENDED,
+        'predicate-public': PREDICATE_PUBLIC,
+        'predicate-private': PREDICATE_PRIVATE,
+      },
+      sorts: {
+        'predicate-due': PREDICATE_DUE_AT,
+        'predicate-created': PREDICATE_CREATED_AT,
+        'predicate-modified': PREDICATE_MODIFIED_AT,
+      },
+      has: {
+        'predicate-description': PREDICATE_DESCRIPTION,
+        'predicate-checklist': PREDICATE_CHECKLIST,
+        'predicate-attachment': PREDICATE_ATTACHMENT,
+        'predicate-start': PREDICATE_START_AT,
+        'predicate-end': PREDICATE_END_AT,
+        'predicate-due': PREDICATE_DUE_AT,
+        'predicate-assignee': PREDICATE_ASSIGNEES,
+        'predicate-member': PREDICATE_MEMBERS,
+      },
+    };
+    const predicateTranslations = {};
+    Object.entries(predicates).forEach(([category, catPreds]) => {
+      predicateTranslations[category] = {};
+      Object.entries(catPreds).forEach(([tag, value]) => {
+        predicateTranslations[category][TAPi18n.__(tag)] = value;
+      });
+    });
+    // eslint-disable-next-line no-console
+    // console.log('predicateTranslations:', predicateTranslations);
+
+    const operatorMap = {};
+    Object.entries(operators).forEach(([key, value]) => {
+      operatorMap[TAPi18n.__(key).toLowerCase()] = value;
+    });
+    // eslint-disable-next-line no-console
+    // console.log('operatorMap:', operatorMap);
+
+    let text = '';
+    while (queryText) {
+      let m = queryText.match(reOperator1);
+      if (!m) {
+        m = queryText.match(reOperator2);
+        if (m) {
+          queryText = queryText.replace(reOperator2, '');
+        }
+      } else {
+        queryText = queryText.replace(reOperator1, '');
+      }
+      if (m) {
+        let op;
+        if (m.groups.operator) {
+          op = m.groups.operator.toLowerCase();
+        } else {
+          op = m.groups.abbrev.toLowerCase();
+        }
+        // eslint-disable-next-line no-prototype-builtins
+        if (operatorMap.hasOwnProperty(op)) {
+          const operator = operatorMap[op];
+          let value = m.groups.value;
+          if (operator === OPERATOR_LABEL) {
+            if (value in this.colorMap) {
+              value = this.colorMap[value];
+              // console.log('found color:', value);
+            }
+          } else if (
+            [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].includes(
+              operator,
+            )
+          ) {
+            const days = parseInt(value, 10);
+            let duration = null;
+            if (isNaN(days)) {
+              // duration was specified as text
+              if (predicateTranslations.durations[value]) {
+                duration = predicateTranslations.durations[value];
+                let date = null;
+                switch (duration) {
+                  case PREDICATE_WEEK:
+                    // eslint-disable-next-line no-case-declarations
+                    const week = moment().week();
+                    if (week === 52) {
+                      date = moment(1, 'W');
+                      date.set('year', date.year() + 1);
+                    } else {
+                      date = moment(week + 1, 'W');
+                    }
+                    break;
+                  case PREDICATE_MONTH:
+                    // eslint-disable-next-line no-case-declarations
+                    const month = moment().month();
+                    // .month() is zero indexed
+                    if (month === 11) {
+                      date = moment(1, 'M');
+                      date.set('year', date.year() + 1);
+                    } else {
+                      date = moment(month + 2, 'M');
+                    }
+                    break;
+                  case PREDICATE_QUARTER:
+                    // eslint-disable-next-line no-case-declarations
+                    const quarter = moment().quarter();
+                    if (quarter === 4) {
+                      date = moment(1, 'Q');
+                      date.set('year', date.year() + 1);
+                    } else {
+                      date = moment(quarter + 1, 'Q');
+                    }
+                    break;
+                  case PREDICATE_YEAR:
+                    date = moment(moment().year() + 1, 'YYYY');
+                    break;
+                }
+                if (date) {
+                  value = {
+                    operator: '$lt',
+                    value: date.format('YYYY-MM-DD'),
+                  };
+                }
+              } else if (
+                operator === OPERATOR_DUE &&
+                value === PREDICATE_OVERDUE
+              ) {
+                value = {
+                  operator: '$lt',
+                  value: moment().format('YYYY-MM-DD'),
+                };
+              } else {
+                this.errors.addError(OPERATOR_DUE, {
+                  tag: 'operator-number-expected',
+                  value: { operator: op, value },
+                });
+                continue;
+              }
+            } else if (operator === OPERATOR_DUE) {
+              value = {
+                operator: '$lt',
+                value: moment(moment().format('YYYY-MM-DD'))
+                  .add(days + 1, duration ? duration : 'days')
+                  .format(),
+              };
+            } else {
+              value = {
+                operator: '$gte',
+                value: moment(moment().format('YYYY-MM-DD'))
+                  .subtract(days, duration ? duration : 'days')
+                  .format(),
+              };
+            }
+          } else if (operator === OPERATOR_SORT) {
+            let negated = false;
+            const m = value.match(reNegatedOperator);
+            if (m) {
+              value = m.groups.operator;
+              negated = true;
+            }
+            if (!predicateTranslations.sorts[value]) {
+              this.errors.addError(OPERATOR_SORT, {
+                tag: 'operator-sort-invalid',
+                value,
+              });
+              continue;
+            } else {
+              value = {
+                name: predicateTranslations.sorts[value],
+                order: negated ? ORDER_DESCENDING : ORDER_ASCENDING,
+              };
+            }
+          } else if (operator === OPERATOR_STATUS) {
+            if (!predicateTranslations.status[value]) {
+              this.errors.addError(OPERATOR_STATUS, {
+                tag: 'operator-status-invalid',
+                value,
+              });
+              continue;
+            } else {
+              value = predicateTranslations.status[value];
+            }
+          } else if (operator === OPERATOR_HAS) {
+            let negated = false;
+            const m = value.match(reNegatedOperator);
+            if (m) {
+              value = m.groups.operator;
+              negated = true;
+            }
+            if (!predicateTranslations.has[value]) {
+              this.errors.addError(OPERATOR_HAS, {
+                tag: 'operator-has-invalid',
+                value,
+              });
+              continue;
+            } else {
+              value = {
+                field: predicateTranslations.has[value],
+                exists: !negated,
+              };
+            }
+          } else if (operator === OPERATOR_LIMIT) {
+            const limit = parseInt(value, 10);
+            if (isNaN(limit) || limit < 1) {
+              this.errors.addError(OPERATOR_LIMIT, {
+                tag: 'operator-limit-invalid',
+                value,
+              });
+              continue;
+            } else {
+              value = limit;
+            }
+          }
+
+          this.queryParams.addPredicate(operator, value);
+        } else {
+          this.errors.addError(OPERATOR_UNKNOWN, {
+            tag: 'operator-unknown-error',
+            value: op,
+          });
+        }
+        continue;
+      }
+
+      m = queryText.match(reQuotedText);
+      if (!m) {
+        m = queryText.match(reText);
+        if (m) {
+          queryText = queryText.replace(reText, '');
+        }
+      } else {
+        queryText = queryText.replace(reQuotedText, '');
+      }
+      if (m) {
+        text += (text ? ' ' : '') + m.groups.text;
+      }
+    }
+
+    // eslint-disable-next-line no-console
+    // console.log('text:', text);
+    this.queryParams.text = text;
+
+    // eslint-disable-next-line no-console
+    console.log('queryParams:', this.queryParams);
+  }
+}

+ 41 - 0
config/search-const.js

@@ -0,0 +1,41 @@
+export const DEFAULT_LIMIT = 25;
+export const OPERATOR_ASSIGNEE = 'assignee';
+export const OPERATOR_COMMENT = 'comment';
+export const OPERATOR_CREATED_AT = 'createdAt';
+export const OPERATOR_DUE = 'dueAt';
+export const OPERATOR_BOARD = 'board';
+export const OPERATOR_HAS = 'has';
+export const OPERATOR_LABEL = 'label';
+export const OPERATOR_LIMIT = 'limit';
+export const OPERATOR_LIST = 'list';
+export const OPERATOR_MEMBER = 'member';
+export const OPERATOR_MODIFIED_AT = 'modifiedAt';
+export const OPERATOR_SORT = 'sort';
+export const OPERATOR_STATUS = 'status';
+export const OPERATOR_SWIMLANE = 'swimlane';
+export const OPERATOR_UNKNOWN = 'unknown';
+export const OPERATOR_USER = 'user';
+export const ORDER_ASCENDING = 'asc';
+export const ORDER_DESCENDING = 'des';
+export const PREDICATE_ALL = 'all';
+export const PREDICATE_ARCHIVED = 'archived';
+export const PREDICATE_ASSIGNEES = 'assignees';
+export const PREDICATE_ATTACHMENT = 'attachment';
+export const PREDICATE_CHECKLIST = 'checklist';
+export const PREDICATE_CREATED_AT = 'createdAt';
+export const PREDICATE_DESCRIPTION = 'description';
+export const PREDICATE_DUE_AT = 'dueAt';
+export const PREDICATE_END_AT = 'endAt';
+export const PREDICATE_ENDED = 'ended';
+export const PREDICATE_MEMBERS = 'members';
+export const PREDICATE_MODIFIED_AT = 'modifiedAt';
+export const PREDICATE_MONTH = 'month';
+export const PREDICATE_OPEN = 'open';
+export const PREDICATE_OVERDUE = 'overdue';
+export const PREDICATE_PRIVATE = 'private';
+export const PREDICATE_PUBLIC = 'public';
+export const PREDICATE_QUARTER = 'quarter';
+export const PREDICATE_START_AT = 'startAt';
+export const PREDICATE_SYSTEM = 'system';
+export const PREDICATE_WEEK = 'week';
+export const PREDICATE_YEAR = 'year';

+ 3 - 1
i18n/en.i18n.json

@@ -947,7 +947,7 @@
   "globalSearch-instructions-operator-swimlane": "`__operator_swimlane__:<title>` - cards in swimlanes matching the specified *<title>*",
   "globalSearch-instructions-operator-swimlane": "`__operator_swimlane__:<title>` - cards in swimlanes matching the specified *<title>*",
   "globalSearch-instructions-operator-comment": "`__operator_comment__:<text>` - cards with a comment containing *<text>*.",
   "globalSearch-instructions-operator-comment": "`__operator_comment__:<text>` - cards with a comment containing *<text>*.",
   "globalSearch-instructions-operator-label": "`__operator_label__:<color>` `__operator_label__:<name>` - cards that have a label matching *<color>* or *<name>",
   "globalSearch-instructions-operator-label": "`__operator_label__:<color>` `__operator_label__:<name>` - cards that have a label matching *<color>* or *<name>",
-  "globalSearch-instructions-operator-hash": "`__operator_label_abbrev__<name | color>` - shorthand for `__operator_label__:<color>` or `__operator_label__:<name>`",
+  "globalSearch-instructions-operator-hash": "`__operator_label_abbrev__<name|color>` - shorthand for `__operator_label__:<color>` or `__operator_label__:<name>`",
   "globalSearch-instructions-operator-user": "`__operator_user__:<username>` - cards where *<username>* is a *member* or *assignee*",
   "globalSearch-instructions-operator-user": "`__operator_user__:<username>` - cards where *<username>* is a *member* or *assignee*",
   "globalSearch-instructions-operator-at": "`__operator_user_abbrev__username` - shorthand for `user:<username>`",
   "globalSearch-instructions-operator-at": "`__operator_user_abbrev__username` - shorthand for `user:<username>`",
   "globalSearch-instructions-operator-member": "`__operator_member__:<username>` - cards where *<username>* is a *member*",
   "globalSearch-instructions-operator-member": "`__operator_member__:<username>` - cards where *<username>* is a *member*",
@@ -979,6 +979,8 @@
   "sort-cards": "Sort Cards",
   "sort-cards": "Sort Cards",
   "cardsSortPopup-title": "Sort Cards",
   "cardsSortPopup-title": "Sort Cards",
   "due-date": "Due Date",
   "due-date": "Due Date",
+  "server-error": "Server Error",
+  "server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`",
   "title-alphabetically": "Title (Alphabetically)",
   "title-alphabetically": "Title (Alphabetically)",
   "created-at-newest-first": "Created At (Newest First)",
   "created-at-newest-first": "Created At (Newest First)",
   "created-at-oldest-first": "Created At (Oldest First)",
   "created-at-oldest-first": "Created At (Oldest First)",

+ 7 - 2
models/lists.js

@@ -407,8 +407,13 @@ Meteor.methods({
     // my lists
     // my lists
     return _.uniq(
     return _.uniq(
       Lists.find(
       Lists.find(
-        { boardId: { $in: Boards.userBoardIds(this.userId) } },
-        { fields: { title: 1 } },
+        {
+          boardId: { $in: Boards.userBoardIds(this.userId) },
+          archived: false,
+        },
+        {
+          fields: { title: 1 },
+        },
       )
       )
         .fetch()
         .fetch()
         .map(list => {
         .map(list => {

+ 241 - 422
server/publications/cards.js

@@ -1,3 +1,50 @@
+import moment from 'moment';
+import Users from '../../models/users';
+import Boards from '../../models/boards';
+import Lists from '../../models/lists';
+import Swimlanes from '../../models/swimlanes';
+import Cards from '../../models/cards';
+import CardComments from '../../models/cardComments';
+import Attachments from '../../models/attachments';
+import Checklists from '../../models/checklists';
+import ChecklistItems from '../../models/checklistItems';
+import SessionData from '../../models/usersessiondata';
+import CustomFields from '../../models/customFields';
+import {
+  DEFAULT_LIMIT,
+  OPERATOR_ASSIGNEE,
+  OPERATOR_BOARD,
+  OPERATOR_COMMENT,
+  OPERATOR_DUE,
+  OPERATOR_HAS,
+  OPERATOR_LABEL,
+  OPERATOR_LIMIT,
+  OPERATOR_LIST,
+  OPERATOR_MEMBER,
+  OPERATOR_SORT,
+  OPERATOR_STATUS,
+  OPERATOR_SWIMLANE,
+  OPERATOR_USER,
+  ORDER_ASCENDING,
+  PREDICATE_ALL,
+  PREDICATE_ARCHIVED,
+  PREDICATE_ASSIGNEES,
+  PREDICATE_ATTACHMENT,
+  PREDICATE_CHECKLIST,
+  PREDICATE_CREATED_AT,
+  PREDICATE_DESCRIPTION,
+  PREDICATE_DUE_AT,
+  PREDICATE_END_AT,
+  PREDICATE_ENDED,
+  PREDICATE_MEMBERS,
+  PREDICATE_MODIFIED_AT,
+  PREDICATE_PRIVATE,
+  PREDICATE_PUBLIC,
+  PREDICATE_START_AT,
+  PREDICATE_SYSTEM,
+} from '../../config/search-const';
+import { QueryErrors, QueryParams, Query } from '../../config/query-classes';
+
 const escapeForRegex = require('escape-string-regexp');
 const escapeForRegex = require('escape-string-regexp');
 
 
 Meteor.publish('card', cardId => {
 Meteor.publish('card', cardId => {
@@ -5,260 +52,50 @@ Meteor.publish('card', cardId => {
   return Cards.find({ _id: cardId });
   return Cards.find({ _id: cardId });
 });
 });
 
 
-Meteor.publish('myCards', function() {
-  const userId = Meteor.userId();
-
-  const archivedBoards = [];
-  Boards.find({ archived: true }).forEach(board => {
-    archivedBoards.push(board._id);
-  });
-
-  const archivedSwimlanes = [];
-  Swimlanes.find({ archived: true }).forEach(swimlane => {
-    archivedSwimlanes.push(swimlane._id);
-  });
-
-  const archivedLists = [];
-  Lists.find({ archived: true }).forEach(list => {
-    archivedLists.push(list._id);
-  });
-
-  selector = {
-    archived: false,
-    boardId: { $nin: archivedBoards },
-    swimlaneId: { $nin: archivedSwimlanes },
-    listId: { $nin: archivedLists },
-    $or: [{ members: userId }, { assignees: userId }],
-  };
+Meteor.publish('myCards', function(sessionId) {
+  const queryParams = new QueryParams();
+  queryParams.addPredicate(OPERATOR_USER, Meteor.user().username);
 
 
-  const cards = Cards.find(selector, {
-    fields: {
-      _id: 1,
-      archived: 1,
-      boardId: 1,
-      swimlaneId: 1,
-      listId: 1,
-      title: 1,
-      type: 1,
-      sort: 1,
-      members: 1,
-      assignees: 1,
-      colors: 1,
-      dueAt: 1,
-    },
-  });
-
-  const boards = [];
-  const swimlanes = [];
-  const lists = [];
-  const users = [];
-
-  cards.forEach(card => {
-    if (card.boardId) boards.push(card.boardId);
-    if (card.swimlaneId) swimlanes.push(card.swimlaneId);
-    if (card.listId) lists.push(card.listId);
-    if (card.members) {
-      card.members.forEach(userId => {
-        users.push(userId);
-      });
-    }
-    if (card.assignees) {
-      card.assignees.forEach(userId => {
-        users.push(userId);
-      });
-    }
-  });
-
-  return [
-    cards,
-    Boards.find({ _id: { $in: boards } }),
-    Swimlanes.find({ _id: { $in: swimlanes } }),
-    Lists.find({ _id: { $in: lists } }),
-    Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
-  ];
+  return findCards(sessionId, buildQuery(queryParams));
 });
 });
 
 
-Meteor.publish('dueCards', function(allUsers = false) {
-  check(allUsers, Boolean);
+// Meteor.publish('dueCards', function(sessionId, allUsers = false) {
+//   check(sessionId, String);
+//   check(allUsers, Boolean);
+//
+//   // eslint-disable-next-line no-console
+//   // console.log('all users:', allUsers);
+//
+//   const queryParams = {
+//     has: [{ field: 'dueAt', exists: true }],
+//     limit: 25,
+//     skip: 0,
+//     sort: { name: 'dueAt', order: 'des' },
+//   };
+//
+//   if (!allUsers) {
+//     queryParams.users = [Meteor.user().username];
+//   }
+//
+//   return buildQuery(sessionId, queryParams);
+// });
+
+Meteor.publish('globalSearch', function(sessionId, params) {
+  check(sessionId, String);
+  check(params, Object);
 
 
   // eslint-disable-next-line no-console
   // eslint-disable-next-line no-console
-  // console.log('all users:', allUsers);
-
-  const user = Users.findOne({ _id: this.userId });
-
-  const archivedBoards = [];
-  Boards.find({ archived: true }).forEach(board => {
-    archivedBoards.push(board._id);
-  });
-
-  const permiitedBoards = [];
-  let selector = {
-    archived: false,
-  };
-
-  selector.$or = [
-    { permission: 'public' },
-    { members: { $elemMatch: { userId: user._id, isActive: true } } },
-  ];
-
-  Boards.find(selector).forEach(board => {
-    permiitedBoards.push(board._id);
-  });
-
-  const archivedSwimlanes = [];
-  Swimlanes.find({ archived: true }).forEach(swimlane => {
-    archivedSwimlanes.push(swimlane._id);
-  });
+  // console.log('queryParams:', params);
 
 
-  const archivedLists = [];
-  Lists.find({ archived: true }).forEach(list => {
-    archivedLists.push(list._id);
-  });
-
-  selector = {
-    archived: false,
-    boardId: { $nin: archivedBoards, $in: permiitedBoards },
-    swimlaneId: { $nin: archivedSwimlanes },
-    listId: { $nin: archivedLists },
-    dueAt: { $ne: null },
-    endAt: null,
-  };
-
-  if (!allUsers) {
-    selector.$or = [{ members: user._id }, { assignees: user._id }];
-  }
-
-  const cards = Cards.find(selector, {
-    fields: {
-      _id: 1,
-      archived: 1,
-      boardId: 1,
-      swimlaneId: 1,
-      listId: 1,
-      title: 1,
-      type: 1,
-      sort: 1,
-      members: 1,
-      assignees: 1,
-      colors: 1,
-      dueAt: 1,
-    },
-  });
-
-  const boards = [];
-  const swimlanes = [];
-  const lists = [];
-  const users = [];
-
-  cards.forEach(card => {
-    if (card.boardId) boards.push(card.boardId);
-    if (card.swimlaneId) swimlanes.push(card.swimlaneId);
-    if (card.listId) lists.push(card.listId);
-    if (card.members) {
-      card.members.forEach(userId => {
-        users.push(userId);
-      });
-    }
-    if (card.assignees) {
-      card.assignees.forEach(userId => {
-        users.push(userId);
-      });
-    }
-  });
-
-  return [
-    cards,
-    Boards.find({ _id: { $in: boards } }),
-    Swimlanes.find({ _id: { $in: swimlanes } }),
-    Lists.find({ _id: { $in: lists } }),
-    Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
-  ];
+  return findCards(sessionId, buildQuery(new QueryParams(params)));
 });
 });
 
 
-Meteor.publish('globalSearch', function(sessionId, queryParams) {
-  check(sessionId, String);
-  check(queryParams, Object);
-
-  // eslint-disable-next-line no-console
-  // console.log('queryParams:', queryParams);
-
+function buildSelector(queryParams) {
   const userId = Meteor.userId();
   const userId = Meteor.userId();
-  // eslint-disable-next-line no-console
-  // console.log('userId:', userId);
-
-  const errors = new (class {
-    constructor() {
-      this.notFound = {
-        boards: [],
-        swimlanes: [],
-        lists: [],
-        labels: [],
-        users: [],
-        members: [],
-        assignees: [],
-        status: [],
-        comments: [],
-      };
 
 
-      this.colorMap = Boards.colorMap();
-    }
-
-    hasErrors() {
-      for (const value of Object.values(this.notFound)) {
-        if (value.length) {
-          return true;
-        }
-      }
-      return false;
-    }
-
-    errorMessages() {
-      const messages = [];
-
-      this.notFound.boards.forEach(board => {
-        messages.push({ tag: 'board-title-not-found', value: board });
-      });
-      this.notFound.swimlanes.forEach(swim => {
-        messages.push({ tag: 'swimlane-title-not-found', value: swim });
-      });
-      this.notFound.lists.forEach(list => {
-        messages.push({ tag: 'list-title-not-found', value: list });
-      });
-      this.notFound.comments.forEach(comments => {
-        comments.forEach(text => {
-          messages.push({ tag: 'comment-not-found', value: text });
-        });
-      });
-      this.notFound.labels.forEach(label => {
-        messages.push({
-          tag: 'label-not-found',
-          value: label,
-          color: Boards.labelColors().includes(label),
-        });
-      });
-      this.notFound.users.forEach(user => {
-        messages.push({ tag: 'user-username-not-found', value: user });
-      });
-      this.notFound.members.forEach(user => {
-        messages.push({ tag: 'user-username-not-found', value: user });
-      });
-      this.notFound.assignees.forEach(user => {
-        messages.push({ tag: 'user-username-not-found', value: user });
-      });
-
-      return messages;
-    }
-  })();
+  const errors = new QueryErrors();
 
 
   let selector = {};
   let selector = {};
-  let skip = 0;
-  if (queryParams.skip) {
-    skip = queryParams.skip;
-  }
-  let limit = 25;
-  if (queryParams.limit) {
-    limit = queryParams.limit;
-  }
 
 
   if (queryParams.selector) {
   if (queryParams.selector) {
     selector = queryParams.selector;
     selector = queryParams.selector;
@@ -267,15 +104,15 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 
 
     let archived = false;
     let archived = false;
     let endAt = null;
     let endAt = null;
-    if (queryParams.status.length) {
-      queryParams.status.forEach(status => {
-        if (status === 'archived') {
+    if (queryParams.hasOperator(OPERATOR_STATUS)) {
+      queryParams.getPredicates(OPERATOR_STATUS).forEach(status => {
+        if (status === PREDICATE_ARCHIVED) {
           archived = true;
           archived = true;
-        } else if (status === 'all') {
+        } else if (status === PREDICATE_ALL) {
           archived = null;
           archived = null;
-        } else if (status === 'ended') {
+        } else if (status === PREDICATE_ENDED) {
           endAt = { $nin: [null, ''] };
           endAt = { $nin: [null, ''] };
-        } else if (['private', 'public'].includes(status)) {
+        } else if ([PREDICATE_PRIVATE, PREDICATE_PUBLIC].includes(status)) {
           boardsSelector.permission = status;
           boardsSelector.permission = status;
         }
         }
       });
       });
@@ -320,9 +157,9 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
       selector.endAt = endAt;
       selector.endAt = endAt;
     }
     }
 
 
-    if (queryParams.boards.length) {
+    if (queryParams.hasOperator(OPERATOR_BOARD)) {
       const queryBoards = [];
       const queryBoards = [];
-      queryParams.boards.forEach(query => {
+      queryParams.hasOperator(OPERATOR_BOARD).forEach(query => {
         const boards = Boards.userSearch(userId, {
         const boards = Boards.userSearch(userId, {
           title: new RegExp(escapeForRegex(query), 'i'),
           title: new RegExp(escapeForRegex(query), 'i'),
         });
         });
@@ -331,16 +168,16 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
             queryBoards.push(board._id);
             queryBoards.push(board._id);
           });
           });
         } else {
         } else {
-          errors.notFound.boards.push(query);
+          errors.addNotFound(OPERATOR_BOARD, query);
         }
         }
       });
       });
 
 
       selector.boardId.$in = queryBoards;
       selector.boardId.$in = queryBoards;
     }
     }
 
 
-    if (queryParams.swimlanes.length) {
+    if (queryParams.hasOperator(OPERATOR_SWIMLANE)) {
       const querySwimlanes = [];
       const querySwimlanes = [];
-      queryParams.swimlanes.forEach(query => {
+      queryParams.getPredicates(OPERATOR_SWIMLANE).forEach(query => {
         const swimlanes = Swimlanes.find({
         const swimlanes = Swimlanes.find({
           title: new RegExp(escapeForRegex(query), 'i'),
           title: new RegExp(escapeForRegex(query), 'i'),
         });
         });
@@ -349,7 +186,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
             querySwimlanes.push(swim._id);
             querySwimlanes.push(swim._id);
           });
           });
         } else {
         } else {
-          errors.notFound.swimlanes.push(query);
+          errors.addNotFound(OPERATOR_SWIMLANE, query);
         }
         }
       });
       });
 
 
@@ -360,9 +197,9 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
       selector.swimlaneId.$in = querySwimlanes;
       selector.swimlaneId.$in = querySwimlanes;
     }
     }
 
 
-    if (queryParams.lists.length) {
+    if (queryParams.hasOperator(OPERATOR_LIST)) {
       const queryLists = [];
       const queryLists = [];
-      queryParams.lists.forEach(query => {
+      queryParams.getPredicates(OPERATOR_LIST).forEach(query => {
         const lists = Lists.find({
         const lists = Lists.find({
           title: new RegExp(escapeForRegex(query), 'i'),
           title: new RegExp(escapeForRegex(query), 'i'),
         });
         });
@@ -371,7 +208,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
             queryLists.push(list._id);
             queryLists.push(list._id);
           });
           });
         } else {
         } else {
-          errors.notFound.lists.push(query);
+          errors.addNotFound(OPERATOR_LIST, query);
         }
         }
       });
       });
 
 
@@ -382,8 +219,10 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
       selector.listId.$in = queryLists;
       selector.listId.$in = queryLists;
     }
     }
 
 
-    if (queryParams.comments.length) {
-      const cardIds = CardComments.textSearch(userId, queryParams.comments).map(
+    if (queryParams.hasOperator(OPERATOR_COMMENT)) {
+      const cardIds = CardComments.textSearch(
+        userId,
+        queryParams.getPredicates(OPERATOR_COMMENT),
         com => {
         com => {
           return com.cardId;
           return com.cardId;
         },
         },
@@ -391,82 +230,75 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
       if (cardIds.length) {
       if (cardIds.length) {
         selector._id = { $in: cardIds };
         selector._id = { $in: cardIds };
       } else {
       } else {
-        errors.notFound.comments.push(queryParams.comments);
+        queryParams.getPredicates(OPERATOR_COMMENT).forEach(comment => {
+          errors.addNotFound(OPERATOR_COMMENT, comment);
+        });
       }
       }
     }
     }
 
 
-    ['dueAt', 'createdAt', 'modifiedAt'].forEach(field => {
-      if (queryParams[field]) {
+    [OPERATOR_DUE, 'createdAt', 'modifiedAt'].forEach(field => {
+      if (queryParams.hasOperator(field)) {
         selector[field] = {};
         selector[field] = {};
-        selector[field][queryParams[field]['operator']] = new Date(
-          queryParams[field]['value'],
-        );
+        const predicate = queryParams.getPredicate(field);
+        selector[field][predicate.operator] = new Date(predicate.value);
       }
       }
     });
     });
 
 
-    const queryMembers = [];
-    const queryAssignees = [];
-    if (queryParams.users.length) {
-      queryParams.users.forEach(query => {
-        const users = Users.find({
-          username: query,
-        });
-        if (users.count()) {
-          users.forEach(user => {
-            queryMembers.push(user._id);
-            queryAssignees.push(user._id);
-          });
-        } else {
-          errors.notFound.users.push(query);
-        }
-      });
-    }
+    const queryUsers = {};
+    queryUsers[OPERATOR_ASSIGNEE] = [];
+    queryUsers[OPERATOR_MEMBER] = [];
 
 
-    if (queryParams.members.length) {
-      queryParams.members.forEach(query => {
+    if (queryParams.hasOperator(OPERATOR_USER)) {
+      queryParams.getPredicates(OPERATOR_USER).forEach(query => {
         const users = Users.find({
         const users = Users.find({
           username: query,
           username: query,
         });
         });
         if (users.count()) {
         if (users.count()) {
           users.forEach(user => {
           users.forEach(user => {
-            queryMembers.push(user._id);
+            queryUsers[OPERATOR_MEMBER].push(user._id);
+            queryUsers[OPERATOR_ASSIGNEE].push(user._id);
           });
           });
         } else {
         } else {
-          errors.notFound.members.push(query);
+          errors.addNotFound(OPERATOR_USER, 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);
+    [OPERATOR_MEMBER, OPERATOR_ASSIGNEE].forEach(key => {
+      if (queryParams.hasOperator(key)) {
+        queryParams.getPredicates(key).forEach(query => {
+          const users = Users.find({
+            username: query,
           });
           });
-        } else {
-          errors.notFound.assignees.push(query);
-        }
-      });
-    }
+          if (users.count()) {
+            users.forEach(user => {
+              queryUsers[key].push(user._id);
+            });
+          } else {
+            errors.addNotFound(key, query);
+          }
+        });
+      }
+    });
 
 
-    if (queryMembers.length && queryAssignees.length) {
+    if (
+      queryUsers[OPERATOR_MEMBER].length &&
+      queryUsers[OPERATOR_ASSIGNEE].length
+    ) {
       selector.$and.push({
       selector.$and.push({
         $or: [
         $or: [
-          { members: { $in: queryMembers } },
-          { assignees: { $in: queryAssignees } },
+          { members: { $in: queryUsers[OPERATOR_MEMBER] } },
+          { assignees: { $in: queryUsers[OPERATOR_ASSIGNEE] } },
         ],
         ],
       });
       });
-    } else if (queryMembers.length) {
-      selector.members = { $in: queryMembers };
-    } else if (queryAssignees.length) {
-      selector.assignees = { $in: queryAssignees };
+    } else if (queryUsers[OPERATOR_MEMBER].length) {
+      selector.members = { $in: queryUsers[OPERATOR_MEMBER] };
+    } else if (queryUsers[OPERATOR_ASSIGNEE].length) {
+      selector.assignees = { $in: queryUsers[OPERATOR_ASSIGNEE] };
     }
     }
 
 
-    if (queryParams.labels.length) {
-      queryParams.labels.forEach(label => {
+    if (queryParams.hasOperator(OPERATOR_LABEL)) {
+      queryParams.getPredicates(OPERATOR_LABEL).forEach(label => {
         const queryLabels = [];
         const queryLabels = [];
 
 
         let boards = Boards.userSearch(userId, {
         let boards = Boards.userSearch(userId, {
@@ -511,39 +343,47 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
                 });
                 });
             });
             });
           } else {
           } else {
-            errors.notFound.labels.push(label);
+            errors.addNotFound(OPERATOR_LABEL, label);
           }
           }
         }
         }
 
 
-        selector.labelIds = { $in: queryLabels };
+        selector.labelIds = { $in: _.uniq(queryLabels) };
       });
       });
     }
     }
 
 
-    if (queryParams.has.length) {
-      queryParams.has.forEach(has => {
+    if (queryParams.hasOperator(OPERATOR_HAS)) {
+      queryParams.getPredicates(OPERATOR_HAS).forEach(has => {
         switch (has.field) {
         switch (has.field) {
-          case 'attachment':
-            const attachments = Attachments.find({}, { fields: { cardId: 1 } });
+          case PREDICATE_ATTACHMENT:
             selector.$and.push({
             selector.$and.push({
-              _id: { $in: attachments.map(a => a.cardId) },
+              _id: {
+                $in: Attachments.find({}, { fields: { cardId: 1 } }).map(
+                  a => a.cardId,
+                ),
+              },
             });
             });
             break;
             break;
-          case 'checklist':
-            const checklists = Checklists.find({}, { fields: { cardId: 1 } });
-            selector.$and.push({ _id: { $in: checklists.map(a => a.cardId) } });
+          case PREDICATE_CHECKLIST:
+            selector.$and.push({
+              _id: {
+                $in: Checklists.find({}, { fields: { cardId: 1 } }).map(
+                  a => a.cardId,
+                ),
+              },
+            });
             break;
             break;
-          case 'description':
-          case 'startAt':
-          case 'dueAt':
-          case 'endAt':
+          case PREDICATE_DESCRIPTION:
+          case PREDICATE_START_AT:
+          case PREDICATE_DUE_AT:
+          case PREDICATE_END_AT:
             if (has.exists) {
             if (has.exists) {
               selector[has.field] = { $exists: true, $nin: [null, ''] };
               selector[has.field] = { $exists: true, $nin: [null, ''] };
             } else {
             } else {
               selector[has.field] = { $in: [null, ''] };
               selector[has.field] = { $in: [null, ''] };
             }
             }
             break;
             break;
-          case 'assignees':
-          case 'members':
+          case PREDICATE_ASSIGNEES:
+          case PREDICATE_MEMBERS:
             if (has.exists) {
             if (has.exists) {
               selector[has.field] = { $exists: true, $nin: [null, []] };
               selector[has.field] = { $exists: true, $nin: [null, []] };
             } else {
             } else {
@@ -573,26 +413,26 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 
 
       const attachments = Attachments.find({ 'original.name': regex });
       const attachments = Attachments.find({ 'original.name': regex });
 
 
-      // const comments = CardComments.find(
-      //   { text: regex },
-      //   { fields: { cardId: 1 } },
-      // );
+      const comments = CardComments.find(
+        { text: regex },
+        { fields: { cardId: 1 } },
+      );
 
 
       selector.$and.push({
       selector.$and.push({
         $or: [
         $or: [
           { title: regex },
           { title: regex },
           { description: regex },
           { description: regex },
           { customFields: { $elemMatch: { value: regex } } },
           { customFields: { $elemMatch: { value: regex } } },
-          {
-            _id: {
-              $in: CardComments.textSearch(userId, [queryParams.text]).map(
-                com => com.cardId,
-              ),
-            },
-          },
+          // {
+          //   _id: {
+          //     $in: CardComments.textSearch(userId, [queryParams.text]).map(
+          //       com => com.cardId,
+          //     ),
+          //   },
+          // },
           { _id: { $in: checklists.map(list => list.cardId) } },
           { _id: { $in: checklists.map(list => list.cardId) } },
           { _id: { $in: attachments.map(attach => attach.cardId) } },
           { _id: { $in: attachments.map(attach => attach.cardId) } },
-          // { _id: { $in: comments.map(com => com.cardId) } },
+          { _id: { $in: comments.map(com => com.cardId) } },
         ],
         ],
       });
       });
     }
     }
@@ -607,6 +447,29 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
   // eslint-disable-next-line no-console
   // eslint-disable-next-line no-console
   // console.log('selector.$and:', selector.$and);
   // console.log('selector.$and:', selector.$and);
 
 
+  const query = new Query();
+  query.selector = selector;
+  query.params = queryParams;
+  query._errors = errors;
+
+  return query;
+}
+
+function buildProjection(query) {
+  let skip = 0;
+  if (query.params.skip) {
+    skip = query.params.skip;
+  }
+  let limit = DEFAULT_LIMIT;
+  const configLimit = parseInt(process.env.RESULTS_PER_PAGE, 10);
+  if (!isNaN(configLimit) && configLimit > 0) {
+    limit = configLimit;
+  }
+
+  if (query.params.hasOperator(OPERATOR_LIMIT)) {
+    limit = query.params.getPredicate(OPERATOR_LIMIT);
+  }
+
   const projection = {
   const projection = {
     fields: {
     fields: {
       _id: 1,
       _id: 1,
@@ -636,10 +499,13 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
     limit,
     limit,
   };
   };
 
 
-  if (queryParams.sort) {
-    const order = queryParams.sort.order === 'asc' ? 1 : -1;
-    switch (queryParams.sort.name) {
-      case 'dueAt':
+  if (query.params.hasOperator(OPERATOR_SORT)) {
+    const order =
+      query.params.getPredicate(OPERATOR_SORT).order === ORDER_ASCENDING
+        ? 1
+        : -1;
+    switch (query.params.getPredicate(OPERATOR_SORT).name) {
+      case PREDICATE_DUE_AT:
         projection.sort = {
         projection.sort = {
           dueAt: order,
           dueAt: order,
           boardId: 1,
           boardId: 1,
@@ -648,7 +514,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
           sort: 1,
           sort: 1,
         };
         };
         break;
         break;
-      case 'modifiedAt':
+      case PREDICATE_MODIFIED_AT:
         projection.sort = {
         projection.sort = {
           modifiedAt: order,
           modifiedAt: order,
           boardId: 1,
           boardId: 1,
@@ -657,7 +523,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
           sort: 1,
           sort: 1,
         };
         };
         break;
         break;
-      case 'createdAt':
+      case PREDICATE_CREATED_AT:
         projection.sort = {
         projection.sort = {
           createdAt: order,
           createdAt: order,
           boardId: 1,
           boardId: 1,
@@ -666,7 +532,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
           sort: 1,
           sort: 1,
         };
         };
         break;
         break;
-      case 'system':
+      case PREDICATE_SYSTEM:
         projection.sort = {
         projection.sort = {
           boardId: order,
           boardId: order,
           swimlaneId: order,
           swimlaneId: order,
@@ -681,77 +547,31 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
   // eslint-disable-next-line no-console
   // eslint-disable-next-line no-console
   // console.log('projection:', projection);
   // console.log('projection:', projection);
 
 
-  return findCards(sessionId, selector, projection, errors);
-});
-
-Meteor.publish('brokenCards', function() {
-  const user = Users.findOne({ _id: this.userId });
-
-  const permiitedBoards = [null];
-  let selector = {};
-  selector.$or = [
-    { permission: 'public' },
-    { members: { $elemMatch: { userId: user._id, isActive: true } } },
-  ];
+  query.projection = projection;
 
 
-  Boards.find(selector).forEach(board => {
-    permiitedBoards.push(board._id);
-  });
+  return query;
+}
 
 
-  selector = {
-    boardId: { $in: permiitedBoards },
-    $or: [
-      { boardId: { $in: [null, ''] } },
-      { swimlaneId: { $in: [null, ''] } },
-      { listId: { $in: [null, ''] } },
-    ],
-  };
+function buildQuery(queryParams) {
+  const query = buildSelector(queryParams);
 
 
-  const cards = Cards.find(selector, {
-    fields: {
-      _id: 1,
-      archived: 1,
-      boardId: 1,
-      swimlaneId: 1,
-      listId: 1,
-      title: 1,
-      type: 1,
-      sort: 1,
-      members: 1,
-      assignees: 1,
-      colors: 1,
-      dueAt: 1,
-    },
-  });
+  return buildProjection(query);
+}
 
 
-  const boards = [];
-  const swimlanes = [];
-  const lists = [];
-  const users = [];
-
-  cards.forEach(card => {
-    if (card.boardId) boards.push(card.boardId);
-    if (card.swimlaneId) swimlanes.push(card.swimlaneId);
-    if (card.listId) lists.push(card.listId);
-    if (card.members) {
-      card.members.forEach(userId => {
-        users.push(userId);
-      });
-    }
-    if (card.assignees) {
-      card.assignees.forEach(userId => {
-        users.push(userId);
-      });
-    }
-  });
+Meteor.publish('brokenCards', function(sessionId) {
+  check(sessionId, String);
 
 
-  return [
-    cards,
-    Boards.find({ _id: { $in: boards } }),
-    Swimlanes.find({ _id: { $in: swimlanes } }),
-    Lists.find({ _id: { $in: lists } }),
-    Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
+  const params = new QueryParams();
+  params.addPredicate(OPERATOR_STATUS, PREDICATE_ALL);
+  const query = buildQuery(params);
+  query.selector.$or = [
+    { boardId: { $in: [null, ''] } },
+    { swimlaneId: { $in: [null, ''] } },
+    { listId: { $in: [null, ''] } },
   ];
   ];
+  // console.log('brokenCards selector:', query.selector);
+
+  return findCards(sessionId, query);
 });
 });
 
 
 Meteor.publish('nextPage', function(sessionId) {
 Meteor.publish('nextPage', function(sessionId) {
@@ -761,7 +581,7 @@ Meteor.publish('nextPage', function(sessionId) {
   const projection = session.getProjection();
   const projection = session.getProjection();
   projection.skip = session.lastHit;
   projection.skip = session.lastHit;
 
 
-  return findCards(sessionId, session.getSelector(), projection);
+  return findCards(sessionId, new Query(session.getSelector(), projection));
 });
 });
 
 
 Meteor.publish('previousPage', function(sessionId) {
 Meteor.publish('previousPage', function(sessionId) {
@@ -771,19 +591,20 @@ Meteor.publish('previousPage', function(sessionId) {
   const projection = session.getProjection();
   const projection = session.getProjection();
   projection.skip = session.lastHit - session.resultsCount - projection.limit;
   projection.skip = session.lastHit - session.resultsCount - projection.limit;
 
 
-  return findCards(sessionId, session.getSelector(), projection);
+  return findCards(sessionId, new Query(session.getSelector(), projection));
 });
 });
 
 
-function findCards(sessionId, selector, projection, errors = null) {
+function findCards(sessionId, query) {
   const userId = Meteor.userId();
   const userId = Meteor.userId();
 
 
   // eslint-disable-next-line no-console
   // eslint-disable-next-line no-console
-  // console.log('selector:', selector);
+  // console.log('selector:', query.selector);
+  // console.log('selector.$and:', query.selector.$and);
   // eslint-disable-next-line no-console
   // eslint-disable-next-line no-console
   // console.log('projection:', projection);
   // console.log('projection:', projection);
   let cards;
   let cards;
-  if (!errors || !errors.hasErrors()) {
-    cards = Cards.find(selector, projection);
+  if (!query.hasErrors()) {
+    cards = Cards.find(query.selector, query.projection);
   }
   }
   // eslint-disable-next-line no-console
   // eslint-disable-next-line no-console
   // console.log('count:', cards.count());
   // console.log('count:', cards.count());
@@ -794,19 +615,17 @@ function findCards(sessionId, selector, projection, errors = null) {
       lastHit: 0,
       lastHit: 0,
       resultsCount: 0,
       resultsCount: 0,
       cards: [],
       cards: [],
-      selector: SessionData.pickle(selector),
-      projection: SessionData.pickle(projection),
+      selector: SessionData.pickle(query.selector),
+      projection: SessionData.pickle(query.projection),
+      errors: query.errors(),
     },
     },
   };
   };
-  if (errors) {
-    update.$set.errors = errors.errorMessages();
-  }
 
 
   if (cards) {
   if (cards) {
     update.$set.totalHits = cards.count();
     update.$set.totalHits = cards.count();
     update.$set.lastHit =
     update.$set.lastHit =
-      projection.skip + projection.limit < cards.count()
-        ? projection.skip + projection.limit
+      query.projection.skip + query.projection.limit < cards.count()
+        ? query.projection.skip + query.projection.limit
         : cards.count();
         : cards.count();
     update.$set.cards = cards.map(card => {
     update.$set.cards = cards.map(card => {
       return card._id;
       return card._id;
@@ -884,5 +703,5 @@ function findCards(sessionId, selector, projection, errors = null) {
     ];
     ];
   }
   }
 
 
-  return [SessionData.find({ userId: userId, sessionId })];
+  return [SessionData.find({ userId, sessionId })];
 }
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
snap-src/bin/config


Некоторые файлы не были показаны из-за большого количества измененных файлов