diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 61ef2f2c4..989075d02 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -13,7 +13,7 @@ template(name="globalSearchModalTitle") template(name="globalSearch") if currentUser .wrapper - form.global-search-instructions.js-search-query-form + form.global-search-page.js-search-query-form input.global-search-query-input( id="global-search-input" type="text" @@ -48,29 +48,31 @@ template(name="globalSearch") button.js-next-page | {{_ 'next-page' }} else - .global-search-instructions - h2 {{_ 'boards' }} - .lists-wrapper - each title in myBoardNames.get - span.card-label.list-title.js-board-title - = title - h2 {{_ 'lists' }} - .lists-wrapper - each title in myLists.get - span.card-label.list-title.js-list-title - = title - h2 {{_ 'label-colors' }} - .palette-colors: each label in labelColors - span.card-label.palette-color.js-label-color(class="card-label-{{label.color}}") - = label.name - if myLabelNames.get.length - h2 {{_ 'label-names' }} + .global-search-page + .global-search-help + h2 {{_ 'boards' }} .lists-wrapper - each name in myLabelNames.get - span.card-label.list-title.js-label-name - = name - +viewer - = searchInstructions + each title in myBoardNames.get + span.card-label.list-title.js-board-title + = title + h2 {{_ 'lists' }} + .lists-wrapper + each title in myLists.get + span.card-label.list-title.js-list-title + = title + h2 {{_ 'label-colors' }} + .palette-colors: each label in labelColors + span.card-label.palette-color.js-label-color(class="card-label-{{label.color}}") + = label.name + if myLabelNames.get.length + h2 {{_ 'label-names' }} + .lists-wrapper + each name in myLabelNames.get + span.card-label.list-title.js-label-name + = name + .global-search-instructions + +viewer + = searchInstructions template(name="globalSearchViewChangePopup") if currentUser diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index a6013baf2..7a307b10a 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -116,7 +116,9 @@ BlazeComponent.extendComponent({ // eslint-disable-next-line no-console // console.log('selector:', sessionData.getSelector()); // console.log('session data:', sessionData); - const cards = Cards.find({ _id: { $in: sessionData.cards } }); + 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); @@ -201,6 +203,7 @@ BlazeComponent.extendComponent({ '^(?["\'])(?.*?)\\k(\\s+|$)', 'u', ); + const reNegatedOperator = new RegExp('^-(?.*)$'); const operators = { 'operator-board': 'boards', @@ -223,6 +226,8 @@ BlazeComponent.extendComponent({ 'operator-modified': 'modifiedAt', 'operator-comment': 'comments', 'operator-has': 'has', + 'operator-sort': 'sort', + 'operator-limit': 'limit', }; const predicates = { @@ -238,6 +243,7 @@ BlazeComponent.extendComponent({ status: { 'predicate-archived': 'archived', 'predicate-all': 'all', + 'predicate-open': 'open', 'predicate-ended': 'ended', 'predicate-public': 'public', 'predicate-private': 'private', @@ -251,6 +257,11 @@ BlazeComponent.extendComponent({ 'predicate-description': 'description', 'predicate-checklist': 'checklist', 'predicate-attachment': 'attachment', + 'predicate-start': 'startAt', + 'predicate-end': 'endAt', + 'predicate-due': 'dueAt', + 'predicate-assignee': 'assignees', + 'predicate-member': 'members', }, }; const predicateTranslations = {}; @@ -307,25 +318,65 @@ BlazeComponent.extendComponent({ } // eslint-disable-next-line no-prototype-builtins if (operatorMap.hasOwnProperty(op)) { + const operator = operatorMap[op]; let value = m.groups.value; - if (operatorMap[op] === 'labels') { + if (operator === 'labels') { if (value in this.colorMap) { value = this.colorMap[value]; // console.log('found color:', value); } - } else if ( - ['dueAt', 'createdAt', 'modifiedAt'].includes(operatorMap[op]) - ) { + } 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]; - value = moment(); - } else if (predicateTranslations.due[value] === 'overdue') { - value = moment(); - duration = 'days'; - days = 0; + 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', @@ -334,27 +385,41 @@ BlazeComponent.extendComponent({ value = null; } } else { - value = moment(); - } - if (value) { - if (operatorMap[op] === 'dueAt') { - value = value.add(days, duration ? duration : 'days').format(); + if (operator === 'dueAt') { + value = { + operator: '$lt', + value: moment(moment().format('YYYY-MM-DD')) + .add(days + 1, duration ? duration : 'days') + .format(), + }; } else { - value = value - .subtract(days, duration ? duration : 'days') - .format(); + value = { + operator: '$gte', + value: moment(moment().format('YYYY-MM-DD')) + .subtract(days, duration ? duration : 'days') + .format(), + }; } } - } else if (operatorMap[op] === 'sort') { + } 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 = predicateTranslations.sorts[value]; + value = { + name: predicateTranslations.sorts[value], + order: negated ? 'des' : 'asc', + }; } - } else if (operatorMap[op] === 'status') { + } else if (operator === 'status') { if (!predicateTranslations.status[value]) { this.parsingErrors.push({ tag: 'operator-status-invalid', @@ -363,20 +428,39 @@ BlazeComponent.extendComponent({ } else { value = predicateTranslations.status[value]; } - } else if (operatorMap[op] === 'has') { + } else if (operator === 'has') { + let negated = false; + const m = value.match(reNegatedOperator); + if (m) { + value = m.groups.operator; + negated = true; + } if (!predicateTranslations.has[value]) { this.parsingErrors.push({ tag: 'operator-has-invalid', value, }); } else { - value = predicateTranslations.has[value]; + value = { + field: predicateTranslations.has[value], + exists: !negated, + }; + } + } else if (operator === 'limit') { + const limit = parseInt(value, 10); + if (isNaN(limit) || limit < 1) { + this.parsingErrors.push({ + tag: 'operator-limit-invalid', + value, + }); + } else { + value = limit; } } - if (Array.isArray(params[operatorMap[op]])) { - params[operatorMap[op]].push(value); + if (Array.isArray(params[operator])) { + params[operator].push(value); } else { - params[operatorMap[op]] = value; + params[operator] = value; } } else { this.parsingErrors.push({ @@ -437,20 +521,10 @@ BlazeComponent.extendComponent({ }, nextPage() { - sessionData = this.getSessionData(); - - const params = { - limit: this.resultsPerPage, - selector: sessionData.getSelector(), - skip: sessionData.lastHit, - }; + const sessionData = this.getSessionData(); this.autorun(() => { - const handle = Meteor.subscribe( - 'globalSearch', - SessionData.getSessionId(), - params, - ); + const handle = Meteor.subscribe('nextPage', sessionData.sessionId); Tracker.nonreactive(() => { Tracker.autorun(() => { if (handle.ready()) { @@ -464,21 +538,10 @@ BlazeComponent.extendComponent({ }, previousPage() { - sessionData = this.getSessionData(); - - const params = { - limit: this.resultsPerPage, - selector: sessionData.getSelector(), - skip: - sessionData.lastHit - sessionData.resultsCount - this.resultsPerPage, - }; + const sessionData = this.getSessionData(); this.autorun(() => { - const handle = Meteor.subscribe( - 'globalSearch', - SessionData.getSessionId(), - params, - ); + const handle = Meteor.subscribe('previousPage', sessionData.sessionId); Tracker.nonreactive(() => { Tracker.autorun(() => { if (handle.ready()) { @@ -531,6 +594,8 @@ BlazeComponent.extendComponent({ operator_modified: TAPi18n.__('operator-modified'), operator_status: TAPi18n.__('operator-status'), operator_has: TAPi18n.__('operator-has'), + operator_sort: TAPi18n.__('operator-sort'), + operator_limit: TAPi18n.__('operator-limit'), predicate_overdue: TAPi18n.__('predicate-overdue'), predicate_archived: TAPi18n.__('predicate-archived'), predicate_all: TAPi18n.__('predicate-all'), @@ -544,81 +609,67 @@ BlazeComponent.extendComponent({ predicate_checklist: TAPi18n.__('predicate-checklist'), predicate_public: TAPi18n.__('predicate-public'), predicate_private: TAPi18n.__('predicate-private'), + predicate_due: TAPi18n.__('predicate-due'), + predicate_created: TAPi18n.__('predicate-created'), + predicate_modified: TAPi18n.__('predicate-modified'), + predicate_start: TAPi18n.__('predicate-start'), + predicate_end: TAPi18n.__('predicate-end'), + predicate_assignee: TAPi18n.__('predicate-assignee'), + predicate_member: TAPi18n.__('predicate-member'), }; - text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; + let text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; text += `\n${TAPi18n.__('globalSearch-instructions-description', tags)}`; - text += `\n${TAPi18n.__('globalSearch-instructions-operators', tags)}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-board', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-list', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-swimlane', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-comment', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-label', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-hash', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-user', - tags, - )}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-at', tags)}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-member', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-assignee', - tags, - )}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-due', tags)}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-created', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-operator-modified', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-status-archived', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-status-public', - tags, - )}`; - text += `\n* ${TAPi18n.__( - 'globalSearch-instructions-status-private', - tags, - )}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-status-all', tags)}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-status-ended', tags)}`; + text += `\n\n${TAPi18n.__('globalSearch-instructions-operators', tags)}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-has', 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')}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3', tags)}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3-2', tags)}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-4', tags)}`; - text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-5', tags)}`; + [ + '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)}`; + }); return text; }, diff --git a/client/components/main/globalSearch.styl b/client/components/main/globalSearch.styl index e460f506e..66115d86c 100644 --- a/client/components/main/globalSearch.styl +++ b/client/components/main/globalSearch.styl @@ -71,17 +71,17 @@ .global-search-error-messages color: darkred -.global-search-instructions +.global-search-page width: 40% min-width: 400px margin-right: auto margin-left: auto line-height: 150% -.global-search-instructions h1 +.global-search-page h1 margin-top: 2rem; -.global-search-instructions h2 +.global-search-page h2 margin-top: 1rem; .global-search-query-input @@ -100,7 +100,7 @@ code color: black background-color: lightgrey padding: 0.1rem !important - font-size: 0.7rem !important + font-size: 0.8rem !important .list-title background-color: darkgray @@ -116,3 +116,6 @@ code .global-search-previous-page border: none text-align: left; + +.global-search-instructions li + margin-bottom: 0.3rem diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index e2ec59655..0d435439e 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -907,7 +907,9 @@ "operator-sort": "sort", "operator-comment": "comment", "operator-has": "has", + "operator-limit": "limit", "predicate-archived": "archived", + "predicate-open": "open", "predicate-ended": "ended", "predicate-all": "all", "predicate-overdue": "overdue", @@ -921,6 +923,10 @@ "predicate-attachment": "attachment", "predicate-description": "description", "predicate-checklist": "checklist", + "predicate-start": "start", + "predicate-end": "end", + "predicate-assignee": "assignee", + "predicate-member": "member", "predicate-public": "public", "predicate-private": "private", "operator-unknown-error": "%s is not an operator", @@ -928,35 +934,39 @@ "operator-sort-invalid": "sort of '%s' is invalid", "operator-status-invalid": "'%s' is not a valid status", "operator-has-invalid": "%s is not a valid existence check", + "operator-limit-invalid": "%s is not a valid limit. Limit should be a positive integer.", "next-page": "Next Page", "previous-page": "Previous Page", "heading-notes": "Notes", "globalSearch-instructions-heading": "Search Instructions", "globalSearch-instructions-description": "Searches can include operators to refine the search. Operators are specified by writing the operator name and value separated by a colon. For example, an operator specification of `list:Blocked` would limit the search to cards that are contained in a list named *Blocked*. If the value contains spaces or special characters it must be enclosed in quotation marks (e.g. `__operator_list__:\"To Review\"`).", "globalSearch-instructions-operators": "Available operators:", - "globalSearch-instructions-operator-board": "`__operator_board__:title` - cards in boards matching the specified title", - "globalSearch-instructions-operator-list": "`__operator_list__:title` - cards in lists 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-label": "`__operator_label__:color` `__operator_label__:name` - cards that have a label matching the given color or name", - "globalSearch-instructions-operator-hash": "`__operator_label_abbrev__label` - shorthand for `__operator_label__:label`", - "globalSearch-instructions-operator-user": "`__operator_user__:username` - cards where the specified user is a *member* or *assignee*", - "globalSearch-instructions-operator-at": "`__operator_user_abbrev__username` - shorthand for `user:username`", - "globalSearch-instructions-operator-member": "`__operator_member__:username` - cards where the specified user is a *member*", - "globalSearch-instructions-operator-assignee": "`__operator_assignee__:username` - cards where the specified user is an *assignee*", - "globalSearch-instructions-operator-due": "`__operator_due__:n` - cards which are due *n* days from now. `__operator_due__:__predicate_overdue__ lists all cards past their due date.", - "globalSearch-instructions-operator-created": "`__operator_created__:n` - cards which were created *n* days ago", - "globalSearch-instructions-operator-modified": "`__operator_modified__:n` - cards which were modified *n* days ago", - "globalSearch-instructions-status-archived": "`__operator_status__:__predicate_archived__` - cards that are archived.", - "globalSearch-instructions-status-all": "`__operator_status__:__predicate_all__` - all archived and unarchived cards.", - "globalSearch-instructions-status-ended": "`__operator_status__:__predicate_ended__` - cards with an end date.", - "globalSearch-instructions-status-public": "`__operator_status__:__predicate_public__` - cards only in public boards.", - "globalSearch-instructions-status-private": "`__operator_status__:__predicate_private__` - cards only in private boards.", - "globalSearch-instructions-operator-has": "`__operator_has__:field` - where *field* is one of `__predicate_attachment__`, `__predicate_checklist__` or `__predicate_description__`", + "globalSearch-instructions-operator-board": "`__operator_board__:` - cards in boards matching the specified *<title>*", + "globalSearch-instructions-operator-list": "`__operator_list__:<title>` - cards in lists 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-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-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-member": "`__operator_member__:<username>` - cards where *<username>* is a *member*", + "globalSearch-instructions-operator-assignee": "`__operator_assignee__:<username>` - cards where *<username>* is an *assignee*", + "globalSearch-instructions-operator-due": "`__operator_due__:<n>` - cards which are due up to *<n>* days from now. `__operator_due__:__predicate_overdue__ lists all cards past their due date.", + "globalSearch-instructions-operator-created": "`__operator_created__:<n>` - cards which were created *<n>* days ago or less", + "globalSearch-instructions-operator-modified": "`__operator_modified__:<n>` - cards which were modified *<n>* days ago or less", + "globalSearch-instructions-operator-status": "`__operator_status__:<status>` - where *<status>* is one of the following:", + "globalSearch-instructions-status-archived": "`__predicate_archived__` - archived cards", + "globalSearch-instructions-status-all": "`__predicate_all__` - all archived and unarchived cards", + "globalSearch-instructions-status-ended": "`__predicate_ended__` - cards with an end date", + "globalSearch-instructions-status-public": "`__predicate_public__` - cards only in public boards", + "globalSearch-instructions-status-private": "`__predicate_private__` - cards only in private boards", + "globalSearch-instructions-operator-has": "`__operator_has__:<field>` - where *<field>* is one of `__predicate_attachment__`, `__predicate_checklist__`, `__predicate_description__`, `__predicate_start__`, `__predicate_due__`, `__predicate_end__`, `__predicate_assignee__` or `__predicate_member__`. Placing a `-` in front of *<field>* searches for the absence of a value in that field (e.g. `has:-due` searches for cards without a due date).", + "globalSearch-instructions-operator-sort": "`__operator_sort__:<sort-name>` - where *<sort-name>* is one of `__predicate_due__`, `__predicate_created__` or `__predicate_modified__`. For a descending sort, place a `-` in front of the sort name.", + "globalSearch-instructions-operator-limit": "`__operator_limit__:<n>` - where *<n>* is a positive integer expressing the number of cards to be displayed per page.", "globalSearch-instructions-notes-1": "Multiple operators may be specified.", "globalSearch-instructions-notes-2": "Similar operators are *OR*ed together. Cards that match any of the conditions will be returned.\n`__operator_list__:Available __operator_list__:Blocked` would return cards contained in any list named *Blocked* or *Available*.", "globalSearch-instructions-notes-3": "Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned. `__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.", - "globalSearch-instructions-notes-3-2": "Days can be specified as an integer or using `__predicate_week__`, `__predicate_month__`, `__predicate_quarter__` or `__predicate_year__`", + "globalSearch-instructions-notes-3-2": "Days can be specified as a positive or negative integer or using `__predicate_week__`, `__predicate_month__`, `__predicate_quarter__` or `__predicate_year__` for the current period.", "globalSearch-instructions-notes-4": "Text searches are case insensitive.", "globalSearch-instructions-notes-5": "By default archived cards are not searched.", "link-to-search": "Link to this search", diff --git a/models/cardComments.js b/models/cardComments.js index 64631c15e..e77ae164b 100644 --- a/models/cardComments.js +++ b/models/cardComments.js @@ -117,7 +117,7 @@ CardComments.textSearch = (userId, textArray) => { }; for (const text of textArray) { - selector.$and.push({ text: new RegExp(escapeForRegex(text)) }); + selector.$and.push({ text: new RegExp(escapeForRegex(text), 'i') }); } // eslint-disable-next-line no-console diff --git a/models/usersessiondata.js b/models/usersessiondata.js index 003b35c91..97f507402 100644 --- a/models/usersessiondata.js +++ b/models/usersessiondata.js @@ -62,6 +62,12 @@ SessionData.attachSchema( optional: true, blackbox: true, }, + projection: { + type: String, + optional: true, + blackbox: true, + defaultValue: {}, + }, errorMessages: { type: [String], optional: true, @@ -130,40 +136,80 @@ SessionData.helpers({ getSelector() { return SessionData.unpickle(this.selector); }, + getProjection() { + return SessionData.unpickle(this.projection); + }, }); SessionData.unpickle = pickle => { return JSON.parse(pickle, (key, value) => { - if (value === null) { - return null; - } else if (typeof value === 'object') { - // eslint-disable-next-line no-prototype-builtins - if (value.hasOwnProperty('$$class')) { - if (value.$$class === 'RegExp') { - return new RegExp(value.source, value.flags); - } - } - } - return value; + return unpickleValue(value); }); }; +function unpickleValue(value) { + if (value === null) { + return null; + } else if (typeof value === 'object') { + // eslint-disable-next-line no-prototype-builtins + if (value.hasOwnProperty('$$class')) { + switch (value.$$class) { + case 'RegExp': + return new RegExp(value.source, value.flags); + case 'Date': + return new Date(value.stringValue); + case 'Object': + return unpickleObject(value); + } + } + } + return value; +} + +function unpickleObject(obj) { + const newObject = {}; + Object.entries(obj).forEach(([key, value]) => { + newObject[key] = unpickleValue(value); + }); + return newObject; +} + SessionData.pickle = value => { return JSON.stringify(value, (key, value) => { - if (value === null) { - return null; - } else if (typeof value === 'object') { - if (value.constructor.name === 'RegExp') { + return pickleValue(value); + }); +}; + +function pickleValue(value) { + if (value === null) { + return null; + } else if (typeof value === 'object') { + switch(value.constructor.name) { + case 'RegExp': return { $$class: 'RegExp', source: value.source, flags: value.flags, }; - } + case 'Date': + return { + $$class: 'Date', + stringValue: String(value), + }; + case 'Object': + return pickleObject(value); } - return value; + } + return value; +} + +function pickleObject(obj) { + const newObject = {}; + Object.entries(obj).forEach(([key, value]) => { + newObject[key] = pickleValue(value); }); -}; + return newObject; +} if (!Meteor.isServer) { SessionData.getSessionId = () => { diff --git a/package-lock.json b/package-lock.json index d4c8e1d2c..31ab8c6de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -718,6 +718,19 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + } + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -2384,8 +2397,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -2525,7 +2537,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -2810,7 +2821,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -4941,8 +4951,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "1.2.1", @@ -5625,7 +5634,6 @@ "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, "requires": { "is-core-module": "^2.2.0", "path-parse": "^1.0.6" diff --git a/server/publications/cards.js b/server/publications/cards.js index e61dce1f1..4197fd826 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -395,17 +395,14 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { } } - if (queryParams.dueAt !== null) { - selector.dueAt = { $lte: new Date(queryParams.dueAt) }; - } - - if (queryParams.createdAt !== null) { - selector.createdAt = { $gte: new Date(queryParams.createdAt) }; - } - - if (queryParams.modifiedAt !== null) { - selector.modifiedAt = { $gte: new Date(queryParams.modifiedAt) }; - } + ['dueAt', 'createdAt', 'modifiedAt'].forEach(field => { + if (queryParams[field]) { + selector[field] = {}; + selector[field][queryParams[field]['operator']] = new Date( + queryParams[field]['value'], + ); + } + }); const queryMembers = []; const queryAssignees = []; @@ -521,14 +518,33 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { if (queryParams.has.length) { queryParams.has.forEach(has => { - if (has === 'description') { - selector.description = { $exists: true, $nin: [null, ''] }; - } else if (has === 'attachment') { - const attachments = Attachments.find({}, { fields: { cardId: 1 } }); - selector.$and.push({ _id: { $in: attachments.map(a => a.cardId) } }); - } else if (has === 'checklist') { - const checklists = Checklists.find({}, { fields: { cardId: 1 } }); - selector.$and.push({ _id: { $in: checklists.map(a => a.cardId) } }); + switch (has.field) { + case 'attachment': + const attachments = Attachments.find({}, { fields: { cardId: 1 } }); + selector.$and.push({ _id: { $in: attachments.map(a => a.cardId) } }); + break; + case 'checklist': + const checklists = Checklists.find({}, { fields: { cardId: 1 } }); + selector.$and.push({ _id: { $in: checklists.map(a => a.cardId) } }); + break; + case 'description': + case 'startAt': + case 'dueAt': + case 'endAt': + if (has.exists) { + selector[has.field] = { $exists: true, $nin: [null, ''] }; + } else { + selector[has.field] = { $in: [null, ''] }; + } + break; + case 'assignees': + case 'members': + if (has.exists) { + selector[has.field] = { $exists: true, $nin: [null, []] }; + } else { + selector[has.field] = { $in: [null, []] }; + } + break; } }); } @@ -552,6 +568,11 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { const attachments = Attachments.find({ 'original.name': regex }); + // const comments = CardComments.find( + // { text: regex }, + // { fields: { cardId: 1 } }, + // ); + selector.$and.push({ $or: [ { title: regex }, @@ -566,6 +587,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { }, { _id: { $in: checklists.map(list => list.cardId) } }, { _id: { $in: attachments.map(attach => attach.cardId) } }, + // { _id: { $in: comments.map(com => com.cardId) } }, ], }); } @@ -580,89 +602,207 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { // eslint-disable-next-line no-console // console.log('selector.$and:', selector.$and); - let cards = null; + const projection = { + 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, + createdAt: 1, + modifiedAt: 1, + labelIds: 1, + customFields: 1, + }, + sort: { + boardId: 1, + swimlaneId: 1, + listId: 1, + sort: 1, + }, + skip, + limit, + }; - if (!errors.hasErrors()) { - const projection = { - 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, - createdAt: 1, - modifiedAt: 1, - labelIds: 1, - customFields: 1, - }, - skip, - limit, - }; - - if (queryParams.sort === 'due') { - projection.sort = { - dueAt: 1, - boardId: 1, - swimlaneId: 1, - listId: 1, - sort: 1, - }; - } else if (queryParams.sort === 'modified') { - projection.sort = { - modifiedAt: -1, - boardId: 1, - swimlaneId: 1, - listId: 1, - sort: 1, - }; - } else if (queryParams.sort === 'created') { - projection.sort = { - createdAt: -1, - boardId: 1, - swimlaneId: 1, - listId: 1, - sort: 1, - }; - } else if (queryParams.sort === 'system') { - projection.sort = { - boardId: 1, - swimlaneId: 1, - listId: 1, - modifiedAt: 1, - sort: 1, - }; + if (queryParams.sort) { + const order = queryParams.sort.order === 'asc' ? 1 : -1; + switch (queryParams.sort.name) { + case 'dueAt': + projection.sort = { + dueAt: order, + boardId: 1, + swimlaneId: 1, + listId: 1, + sort: 1, + }; + break; + case 'modifiedAt': + projection.sort = { + modifiedAt: order, + boardId: 1, + swimlaneId: 1, + listId: 1, + sort: 1, + }; + break; + case 'createdAt': + projection.sort = { + createdAt: order, + boardId: 1, + swimlaneId: 1, + listId: 1, + sort: 1, + }; + break; + case 'system': + projection.sort = { + boardId: order, + swimlaneId: order, + listId: order, + modifiedAt: order, + sort: order, + }; + break; } - - // eslint-disable-next-line no-console - // console.log('projection:', projection); - cards = Cards.find(selector, projection); - - // eslint-disable-next-line no-console - // console.log('count:', cards.count()); } + // eslint-disable-next-line no-console + // 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 } } }, + ]; + + Boards.find(selector).forEach(board => { + permiitedBoards.push(board._id); + }); + + selector = { + boardId: { $in: permiitedBoards }, + $or: [ + { boardId: { $in: [null, ''] } }, + { swimlaneId: { $in: [null, ''] } }, + { listId: { $in: [null, ''] } }, + ], + }; + + 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 }), + ]; +}); + +Meteor.publish('nextPage', function(sessionId) { + check(sessionId, String); + + const session = SessionData.findOne({ sessionId }); + const projection = session.getProjection(); + projection.skip = session.lastHit; + + return findCards(sessionId, session.getSelector(), projection); +}); + +Meteor.publish('previousPage', function(sessionId) { + check(sessionId, String); + + const session = SessionData.findOne({ sessionId }); + const projection = session.getProjection(); + projection.skip = session.lastHit - session.resultsCount - projection.limit; + + return findCards(sessionId, session.getSelector(), projection); +}); + +function findCards(sessionId, selector, projection, errors = null) { + const userId = Meteor.userId(); + + // eslint-disable-next-line no-console + // console.log('selector:', selector); + // eslint-disable-next-line no-console + // console.log('projection:', projection); + let cards; + if (!errors || !errors.hasErrors()) { + cards = Cards.find(selector, projection); + } + // eslint-disable-next-line no-console + // console.log('count:', cards.count()); + const update = { $set: { totalHits: 0, lastHit: 0, resultsCount: 0, cards: [], - errors: errors.errorMessages(), selector: SessionData.pickle(selector), + projection: SessionData.pickle(projection), }, }; + if (errors) { + update.$set.errors = errors.errorMessages(); + } if (cards) { update.$set.totalHits = cards.count(); update.$set.lastHit = - skip + limit < cards.count() ? skip + limit : cards.count(); + projection.skip + projection.limit < cards.count() + ? projection.skip + projection.limit + : cards.count(); update.$set.cards = cards.map(card => { return card._id; }); @@ -735,79 +875,9 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { Checklists.find({ cardId: { $in: cards.map(c => c._id) } }), Attachments.find({ cardId: { $in: cards.map(c => c._id) } }), CardComments.find({ cardId: { $in: cards.map(c => c._id) } }), - SessionData.find({ userId: this.userId, sessionId }), + SessionData.find({ userId, sessionId }), ]; } - return [SessionData.find({ userId: this.userId, sessionId })]; -}); - -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 } } }, - ]; - - Boards.find(selector).forEach(board => { - permiitedBoards.push(board._id); - }); - - selector = { - boardId: { $in: permiitedBoards }, - $or: [ - { boardId: { $in: [null, ''] } }, - { swimlaneId: { $in: [null, ''] } }, - { listId: { $in: [null, ''] } }, - ], - }; - - 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 [SessionData.find({ userId: userId, sessionId })]; +}