From 43f40c4085e7e173a599cbfdf5b0bc3291fe6963 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Thu, 25 Feb 2021 18:38:51 +0200 Subject: [PATCH 01/11] Fix sort operator * Add server publications for next and previous page * Add ability to sort ascending or descending --- client/components/main/globalSearch.js | 49 ++-- models/cardComments.js | 2 +- models/usersessiondata.js | 9 + package-lock.json | 22 +- server/publications/cards.js | 331 ++++++++++++++----------- 5 files changed, 239 insertions(+), 174 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index a6013baf2..5bfc34ebc 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -116,7 +116,12 @@ 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 +206,7 @@ BlazeComponent.extendComponent({ '^(?["\'])(?.*?)\\k(\\s+|$)', 'u', ); + const reNegatedOperator = new RegExp('^-(?.*)$'); const operators = { 'operator-board': 'boards', @@ -223,6 +229,7 @@ BlazeComponent.extendComponent({ 'operator-modified': 'modifiedAt', 'operator-comment': 'comments', 'operator-has': 'has', + 'operator-sort': 'sort', }; const predicates = { @@ -346,13 +353,22 @@ BlazeComponent.extendComponent({ } } } else if (operatorMap[op] === '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') { if (!predicateTranslations.status[value]) { @@ -437,20 +453,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 +470,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()) { 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..8bd516897 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,6 +136,9 @@ SessionData.helpers({ getSelector() { return SessionData.unpickle(this.selector); }, + getProjection() { + return SessionData.unpickle(this.projection); + }, }); SessionData.unpickle = pickle => { diff --git a/package-lock.json b/package-lock.json index 4889211cc..ccdf89bf3 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..5f4ad7746 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -552,6 +552,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 +571,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 +586,206 @@ 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) { + // check(selector, Object); + // check(projection, Object); + const userId = Meteor.userId(); + + let cards; + if (!errors || !errors.hasErrors()) { + cards = Cards.find(selector, projection); + } + + console.log('selector:', selector); + console.log('projection:', projection); + 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 +858,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 })]; +} From a3229ea965e73b7b42bdfc2602340e4a41002f19 Mon Sep 17 00:00:00 2001 From: John Supplee Date: Fri, 26 Feb 2021 01:13:45 +0200 Subject: [PATCH 02/11] Fixes for duration predicates --- client/components/main/globalSearch.js | 96 ++++++++++++++++++-------- server/publications/cards.js | 19 +++-- 2 files changed, 77 insertions(+), 38 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 5bfc34ebc..eba361b99 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -118,10 +118,7 @@ BlazeComponent.extendComponent({ // console.log('session data:', sessionData); const projection = sessionData.getProjection(); projection.skip = 0; - const cards = Cards.find( - { _id: { $in: sessionData.cards } }, - projection, - ); + const cards = Cards.find({ _id: { $in: sessionData.cards } }, projection); this.queryErrors = sessionData.errors; if (this.queryErrors.length) { this.hasQueryErrors.set(true); @@ -314,25 +311,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(), + }; + } + } else if (operator === 'dueAt' && value === 'overdue') { + value = { + operator: '$lt', + value: moment().format(), + }; } else { this.parsingErrors.push({ tag: 'operator-number-expected', @@ -341,18 +378,23 @@ 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: '$lte', + value: moment() + .add(days, duration ? duration : 'days') + .format(), + }; } else { - value = value - .subtract(days, duration ? duration : 'days') - .format(); + value = { + operator: '$gte', + value: moment() + .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) { @@ -370,7 +412,7 @@ BlazeComponent.extendComponent({ order: negated ? 'des' : 'asc', }; } - } else if (operatorMap[op] === 'status') { + } else if (operator === 'status') { if (!predicateTranslations.status[value]) { this.parsingErrors.push({ tag: 'operator-status-invalid', @@ -379,7 +421,7 @@ BlazeComponent.extendComponent({ } else { value = predicateTranslations.status[value]; } - } else if (operatorMap[op] === 'has') { + } else if (operator === 'has') { if (!predicateTranslations.has[value]) { this.parsingErrors.push({ tag: 'operator-has-invalid', @@ -389,10 +431,10 @@ BlazeComponent.extendComponent({ value = predicateTranslations.has[value]; } } - 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({ diff --git a/server/publications/cards.js b/server/publications/cards.js index 5f4ad7746..06c90b5e3 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 = []; From 0c1cff52b2253cbdd4062d18df11a40cf9eb3c48 Mon Sep 17 00:00:00 2001 From: John Supplee Date: Fri, 26 Feb 2021 12:52:36 +0200 Subject: [PATCH 03/11] Update explanation of days predicate --- i18n/en.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index e2ec59655..2095b7406 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -956,7 +956,7 @@ "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", From 5fe58dc9ae1dfde4fa6c0b62ad96cd54735f470c Mon Sep 17 00:00:00 2001 From: John Supplee Date: Fri, 26 Feb 2021 13:01:23 +0200 Subject: [PATCH 04/11] Add instructions for sort operator --- client/components/main/globalSearch.js | 2 ++ i18n/en.i18n.json | 1 + 2 files changed, 3 insertions(+) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index eba361b99..176d6d375 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -649,6 +649,8 @@ BlazeComponent.extendComponent({ text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-has', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-sort', tags)}`; + text += `\n## ${TAPi18n.__('heading-notes')}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 2095b7406..e1710db94 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -953,6 +953,7 @@ "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-sort": "`__operator_sort__:sort-name` - where *sort-name* is one of `__predicate_due__`, `__predicate_created__` or `__predicate_modified`.", "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.", From 8e911a42f574201e864863a44e5c5c24eeee8a97 Mon Sep 17 00:00:00 2001 From: John Supplee Date: Fri, 26 Feb 2021 14:11:24 +0200 Subject: [PATCH 05/11] Fixes to due predicates --- client/components/main/globalSearch.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 176d6d375..526f744b6 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -368,7 +368,7 @@ BlazeComponent.extendComponent({ } else if (operator === 'dueAt' && value === 'overdue') { value = { operator: '$lt', - value: moment().format(), + value: moment(moment().format('YYYY-MM-DD')).format(), }; } else { this.parsingErrors.push({ @@ -380,15 +380,15 @@ BlazeComponent.extendComponent({ } else { if (operator === 'dueAt') { value = { - operator: '$lte', - value: moment() - .add(days, duration ? duration : 'days') + operator: '$lt', + value: moment(moment().format('YYYY-MM-DD')) + .add(days + 1, duration ? duration : 'days') .format(), }; } else { value = { operator: '$gte', - value: moment() + value: moment(moment().format('YYYY-MM-DD')) .subtract(days, duration ? duration : 'days') .format(), }; From 62b0d371eee81cbaa7812efbde54c7dcf409b1aa Mon Sep 17 00:00:00 2001 From: John Supplee Date: Fri, 26 Feb 2021 17:31:44 +0200 Subject: [PATCH 06/11] Add new limit operator --- client/components/main/globalSearch.js | 11 +++++++++++ i18n/en.i18n.json | 2 ++ server/publications/cards.js | 5 +++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 526f744b6..1866caf2c 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -227,6 +227,7 @@ BlazeComponent.extendComponent({ 'operator-comment': 'comments', 'operator-has': 'has', 'operator-sort': 'sort', + 'operator-limit': 'limit', }; const predicates = { @@ -430,6 +431,16 @@ BlazeComponent.extendComponent({ } else { value = predicateTranslations.has[value]; } + } 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); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index e1710db94..348a29fea 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -907,6 +907,7 @@ "operator-sort": "sort", "operator-comment": "comment", "operator-has": "has", + "operator-limit": "limit", "predicate-archived": "archived", "predicate-ended": "ended", "predicate-all": "all", @@ -928,6 +929,7 @@ "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", diff --git a/server/publications/cards.js b/server/publications/cards.js index 06c90b5e3..d4737851b 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -755,13 +755,14 @@ function findCards(sessionId, selector, projection, errors = null) { // check(projection, Object); const userId = Meteor.userId(); + console.log('selector:', selector); + console.log('projection:', projection); + let cards; if (!errors || !errors.hasErrors()) { cards = Cards.find(selector, projection); } - console.log('selector:', selector); - console.log('projection:', projection); console.log('count:', cards.count()); const update = { $set: { From 223cc07139eb80a75f16e2660e152dcdc879bf58 Mon Sep 17 00:00:00 2001 From: John Supplee Date: Fri, 26 Feb 2021 18:51:54 +0200 Subject: [PATCH 07/11] Add instructions for limit operator --- client/components/main/globalSearch.js | 7 +++++++ i18n/en.i18n.json | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 1866caf2c..cfd36b863 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -579,6 +579,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'), @@ -592,6 +594,9 @@ 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'), }; text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; @@ -662,6 +667,8 @@ BlazeComponent.extendComponent({ text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-sort', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-limit', tags)}`; + text += `\n## ${TAPi18n.__('heading-notes')}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 348a29fea..13a6849c1 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -955,7 +955,8 @@ "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-sort": "`__operator_sort__:sort-name` - where *sort-name* is one of `__predicate_due__`, `__predicate_created__` or `__predicate_modified`.", + "globalSearch-instructions-operator-sort": "`__operator_sort__:sort-name` - where *sort-name* is one of `__predicate_due__`, `__predicate_created__` or `__predicate_modified__`.", + "globalSearch-instructions-operator-limit": "`__operator_limit__:n` - where *n* is the number of cards to be displayed per page expressed as a positive integer.", "globalSearch-instructions-notes-1": "Multiple operators may be specified.", "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.", From a63a61e7fcc1ecd2f9dba5040237e97918cbe1fe Mon Sep 17 00:00:00 2001 From: John Supplee Date: Fri, 26 Feb 2021 23:19:12 +0200 Subject: [PATCH 08/11] Fix problem with dates in selector being unpickled as a String --- client/components/main/globalSearch.js | 4 +- models/usersessiondata.js | 73 +++++++++++++++++++------- server/publications/cards.js | 8 +-- 3 files changed, 61 insertions(+), 24 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index cfd36b863..154356d2d 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -363,13 +363,13 @@ BlazeComponent.extendComponent({ if (date) { value = { operator: '$lt', - value: date.format(), + value: date.format('YYYY-MM-DD'), }; } } else if (operator === 'dueAt' && value === 'overdue') { value = { operator: '$lt', - value: moment(moment().format('YYYY-MM-DD')).format(), + value: moment().format('YYYY-MM-DD'), }; } else { this.parsingErrors.push({ diff --git a/models/usersessiondata.js b/models/usersessiondata.js index 8bd516897..97f507402 100644 --- a/models/usersessiondata.js +++ b/models/usersessiondata.js @@ -143,36 +143,73 @@ SessionData.helpers({ 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/server/publications/cards.js b/server/publications/cards.js index d4737851b..1b6a8ed0f 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -751,19 +751,19 @@ Meteor.publish('previousPage', function(sessionId) { }); function findCards(sessionId, selector, projection, errors = null) { - // check(selector, Object); - // check(projection, Object); const userId = Meteor.userId(); console.log('selector:', selector); console.log('projection:', projection); - + // if (selector.dueAt) { + // console.log('dueAt:', typeof selector.dueAt.$lt, selector.dueAt.$lt.constructor.name, selector.dueAt.$lt); + // } let cards; if (!errors || !errors.hasErrors()) { cards = Cards.find(selector, projection); } - console.log('count:', cards.count()); + const update = { $set: { totalHits: 0, From eb7fc0fb2624f2a563f665e6df44990109292ada Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 27 Feb 2021 00:56:12 +0200 Subject: [PATCH 09/11] Comment out debugging code --- server/publications/cards.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/publications/cards.js b/server/publications/cards.js index 1b6a8ed0f..062f10e2c 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -753,16 +753,16 @@ Meteor.publish('previousPage', function(sessionId) { function findCards(sessionId, selector, projection, errors = null) { const userId = Meteor.userId(); - console.log('selector:', selector); - console.log('projection:', projection); - // if (selector.dueAt) { - // console.log('dueAt:', typeof selector.dueAt.$lt, selector.dueAt.$lt.constructor.name, selector.dueAt.$lt); - // } + // 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); } - console.log('count:', cards.count()); + // eslint-disable-next-line no-console + // console.log('count:', cards.count()); const update = { $set: { From faa101224a65fe88bdac41920c258dcb34df5db3 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 27 Feb 2021 02:26:58 +0200 Subject: [PATCH 10/11] Add new has predicates for more fields --- client/components/main/globalSearch.js | 21 +++++++++++++++- i18n/en.i18n.json | 7 +++++- server/publications/cards.js | 35 ++++++++++++++++++++------ 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 154356d2d..a251fda04 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -243,6 +243,7 @@ BlazeComponent.extendComponent({ status: { 'predicate-archived': 'archived', 'predicate-all': 'all', + 'predicate-open': 'open', 'predicate-ended': 'ended', 'predicate-public': 'public', 'predicate-private': 'private', @@ -256,6 +257,11 @@ BlazeComponent.extendComponent({ 'predicate-description': 'description', 'predicate-checklist': 'checklist', 'predicate-attachment': 'attachment', + 'predicate-start': 'startAt', + 'predicate-end': 'endAt', + 'predicate-due': 'dueAt', + 'predicate-assignee': 'assignees', + 'predicate-member': 'members', }, }; const predicateTranslations = {}; @@ -423,13 +429,22 @@ BlazeComponent.extendComponent({ value = predicateTranslations.status[value]; } } else if (operator === 'has') { + let negated = false; + const m = value.match(reNegatedOperator); + if (m) { + value = m.groups.operator; + negated = true; + } if (!predicateTranslations.has[value]) { this.parsingErrors.push({ tag: 'operator-has-invalid', value, }); } else { - value = predicateTranslations.has[value]; + value = { + field: predicateTranslations.has[value], + exists: !negated, + }; } } else if (operator === 'limit') { const limit = parseInt(value, 10); @@ -597,6 +612,10 @@ BlazeComponent.extendComponent({ predicate_due: TAPi18n.__('predicate-due'), predicate_created: TAPi18n.__('predicate-created'), predicate_modified: TAPi18n.__('predicate-modified'), + predicate_start: TAPi18n.__('predicate-start'), + predicate_end: TAPi18n.__('predicate-end'), + predicate_assignee: TAPi18n.__('predicate-assignee'), + predicate_member: TAPi18n.__('predicate-member'), }; text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 13a6849c1..cc23a9a37 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -909,6 +909,7 @@ "operator-has": "has", "operator-limit": "limit", "predicate-archived": "archived", + "predicate-open": "open", "predicate-ended": "ended", "predicate-all": "all", "predicate-overdue": "overdue", @@ -922,6 +923,10 @@ "predicate-attachment": "attachment", "predicate-description": "description", "predicate-checklist": "checklist", + "predicate-start": "start", + "predicate-end": "end", + "predicate-assignee": "assignee", + "predicate-member": "member", "predicate-public": "public", "predicate-private": "private", "operator-unknown-error": "%s is not an operator", @@ -954,7 +959,7 @@ "globalSearch-instructions-status-ended": "`__operator_status__:__predicate_ended__` - cards with an end date.", "globalSearch-instructions-status-public": "`__operator_status__:__predicate_public__` - cards only in public boards.", "globalSearch-instructions-status-private": "`__operator_status__:__predicate_private__` - cards only in private boards.", - "globalSearch-instructions-operator-has": "`__operator_has__:field` - where *field* is one of `__predicate_attachment__`, `__predicate_checklist__` or `__predicate_description__`", + "globalSearch-instructions-operator-has": "`__operator_has__:field` - where *field* is one of `__predicate_attachment__`, `__predicate_checklist__`, `__predicate_description__`, `__predicate_start__`, `__predicate_due__`, `__predicate_end__`, `__predicate_assignee__` or `__predicate_member__`", "globalSearch-instructions-operator-sort": "`__operator_sort__:sort-name` - where *sort-name* is one of `__predicate_due__`, `__predicate_created__` or `__predicate_modified__`.", "globalSearch-instructions-operator-limit": "`__operator_limit__:n` - where *n* is the number of cards to be displayed per page expressed as a positive integer.", "globalSearch-instructions-notes-1": "Multiple operators may be specified.", diff --git a/server/publications/cards.js b/server/publications/cards.js index 062f10e2c..4197fd826 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -518,14 +518,33 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { if (queryParams.has.length) { queryParams.has.forEach(has => { - if (has === 'description') { - selector.description = { $exists: true, $nin: [null, ''] }; - } else if (has === 'attachment') { - const attachments = Attachments.find({}, { fields: { cardId: 1 } }); - selector.$and.push({ _id: { $in: attachments.map(a => a.cardId) } }); - } else if (has === 'checklist') { - const checklists = Checklists.find({}, { fields: { cardId: 1 } }); - selector.$and.push({ _id: { $in: checklists.map(a => a.cardId) } }); + switch (has.field) { + case 'attachment': + const attachments = Attachments.find({}, { fields: { cardId: 1 } }); + selector.$and.push({ _id: { $in: attachments.map(a => a.cardId) } }); + break; + case 'checklist': + const checklists = Checklists.find({}, { fields: { cardId: 1 } }); + selector.$and.push({ _id: { $in: checklists.map(a => a.cardId) } }); + break; + case 'description': + case 'startAt': + case 'dueAt': + case 'endAt': + if (has.exists) { + selector[has.field] = { $exists: true, $nin: [null, ''] }; + } else { + selector[has.field] = { $in: [null, ''] }; + } + break; + case 'assignees': + case 'members': + if (has.exists) { + selector[has.field] = { $exists: true, $nin: [null, []] }; + } else { + selector[has.field] = { $in: [null, []] }; + } + break; } }); } From b0e4aedd3d939fb2024bff1753ef49c3a05da558 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 27 Feb 2021 17:02:42 +0200 Subject: [PATCH 11/11] Update search instructions --- client/components/main/globalSearch.jade | 48 ++++++------ client/components/main/globalSearch.js | 93 +++++++++--------------- client/components/main/globalSearch.styl | 11 ++- i18n/en.i18n.json | 43 +++++------ 4 files changed, 88 insertions(+), 107 deletions(-) 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 a251fda04..7a307b10a 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -618,83 +618,58 @@ BlazeComponent.extendComponent({ 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.__( + text += `\n\n${TAPi18n.__('globalSearch-instructions-operators', tags)}`; + + [ '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-at', '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-due', 'globalSearch-instructions-operator-created', - tags, - )}`; - text += `\n* ${TAPi18n.__( 'globalSearch-instructions-operator-modified', - tags, - )}`; - text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-status', + ].forEach(instruction => { + text += `\n* ${TAPi18n.__(instruction, tags)}`; + }); + + [ '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)}`; + 'globalSearch-instructions-status-all', + 'globalSearch-instructions-status-ended', + ].forEach(instruction => { + text += `\n * ${TAPi18n.__(instruction, tags)}`; + }); - text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-has', tags)}`; - - text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-sort', tags)}`; - - text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-limit', 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 cc23a9a37..0d435439e 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -941,27 +941,28 @@ "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__`, `__predicate_description__`, `__predicate_start__`, `__predicate_due__`, `__predicate_end__`, `__predicate_assignee__` or `__predicate_member__`", - "globalSearch-instructions-operator-sort": "`__operator_sort__:sort-name` - where *sort-name* is one of `__predicate_due__`, `__predicate_created__` or `__predicate_modified__`.", - "globalSearch-instructions-operator-limit": "`__operator_limit__:n` - where *n* is the number of cards to be displayed per page expressed as a positive integer.", + "globalSearch-instructions-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.",