diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index fbc61c37f..e8b6870ac 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -1,9 +1,17 @@ import { CardSearchPagedComponent } from '../../lib/cardSearch'; import moment from 'moment'; import { + OPERATOR_ASSIGNEE, OPERATOR_BOARD, - OPERATOR_DUE, OPERATOR_LIST, - OPERATOR_SWIMLANE, OPERATOR_USER, + OPERATOR_DUE, + OPERATOR_HAS, + OPERATOR_LABEL, + OPERATOR_LIST, + OPERATOR_MEMBER, + OPERATOR_SORT, + OPERATOR_STATUS, + OPERATOR_SWIMLANE, + OPERATOR_USER, ORDER_ASCENDING, ORDER_DESCENDING, PREDICATE_ALL, @@ -28,6 +36,7 @@ import { PREDICATE_WEEK, PREDICATE_YEAR, } from '../../../config/search-const'; +import { QueryParams } from "../../../config/query-classes"; // const subManager = new SubsManager(); @@ -169,21 +178,21 @@ class GlobalSearchComponent extends CardSearchPagedComponent { 'operator-swimlane-abbrev': OPERATOR_SWIMLANE, 'operator-list': OPERATOR_LIST, 'operator-list-abbrev': OPERATOR_LIST, - 'operator-label': 'labels', - 'operator-label-abbrev': 'labels', + 'operator-label': OPERATOR_LABEL, + 'operator-label-abbrev': OPERATOR_LABEL, 'operator-user': OPERATOR_USER, 'operator-user-abbrev': OPERATOR_USER, - 'operator-member': 'members', - 'operator-member-abbrev': 'members', - 'operator-assignee': 'assignees', - 'operator-assignee-abbrev': 'assignees', - 'operator-status': 'status', + 'operator-member': OPERATOR_MEMBER, + 'operator-member-abbrev': OPERATOR_MEMBER, + 'operator-assignee': OPERATOR_ASSIGNEE, + 'operator-assignee-abbrev': OPERATOR_ASSIGNEE, + 'operator-status': OPERATOR_STATUS, 'operator-due': OPERATOR_DUE, 'operator-created': 'createdAt', 'operator-modified': 'modifiedAt', 'operator-comment': 'comments', - 'operator-has': 'has', - 'operator-sort': 'sort', + 'operator-has': OPERATOR_HAS, + 'operator-sort': OPERATOR_SORT, 'operator-limit': 'limit', }; @@ -238,28 +247,30 @@ class GlobalSearchComponent extends CardSearchPagedComponent { // eslint-disable-next-line no-console // console.log('operatorMap:', operatorMap); - const params = { - limit: this.resultsPerPage, - // boards: [], - // swimlanes: [], - // lists: [], - // users: [], - members: [], - assignees: [], - labels: [], - status: [], - // dueAt: null, - createdAt: null, - modifiedAt: null, - comments: [], - has: [], - }; - params[OPERATOR_BOARD] = []; - params[OPERATOR_DUE] = null; - params[OPERATOR_LIST] = []; - params[OPERATOR_SWIMLANE] = []; - params[OPERATOR_USER] = []; + // const params = { + // limit: this.resultsPerPage, + // // boards: [], + // // swimlanes: [], + // // lists: [], + // // users: [], + // members: [], + // assignees: [], + // // labels: [], + // status: [], + // // dueAt: null, + // createdAt: null, + // modifiedAt: null, + // comments: [], + // has: [], + // }; + // params[OPERATOR_BOARD] = []; + // params[OPERATOR_DUE] = null; + // params[OPERATOR_LABEL] = []; + // params[OPERATOR_LIST] = []; + // params[OPERATOR_SWIMLANE] = []; + // params[OPERATOR_USER] = []; + const params = new QueryParams(); let text = ''; while (query) { let m = query.match(reOperator1); @@ -282,7 +293,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { if (operatorMap.hasOwnProperty(op)) { const operator = operatorMap[op]; let value = m.groups.value; - if (operator === 'labels') { + if (operator === OPERATOR_LABEL) { if (value in this.colorMap) { value = this.colorMap[value]; // console.log('found color:', value); @@ -366,7 +377,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { .format(), }; } - } else if (operator === 'sort') { + } else if (operator === OPERATOR_SORT) { let negated = false; const m = value.match(reNegatedOperator); if (m) { @@ -384,7 +395,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { order: negated ? ORDER_DESCENDING : ORDER_ASCENDING, }; } - } else if (operator === 'status') { + } else if (operator === OPERATOR_STATUS) { if (!predicateTranslations.status[value]) { this.parsingErrors.push({ tag: 'operator-status-invalid', @@ -393,7 +404,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { } else { value = predicateTranslations.status[value]; } - } else if (operator === 'has') { + } else if (operator === OPERATOR_HAS) { let negated = false; const m = value.match(reNegatedOperator); if (m) { @@ -422,11 +433,8 @@ class GlobalSearchComponent extends CardSearchPagedComponent { value = limit; } } - if (Array.isArray(params[operator])) { - params[operator].push(value); - } else { - params[operator] = value; - } + + params.addPredicate(operator, value); } else { this.parsingErrors.push({ tag: 'operator-unknown-error', @@ -467,7 +475,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { return; } - this.runGlobalSearch(params); + this.runGlobalSearch(params.getParams()); } searchInstructions() { diff --git a/client/components/main/myCards.js b/client/components/main/myCards.js index 9307b9d32..0a2794e5b 100644 --- a/client/components/main/myCards.js +++ b/client/components/main/myCards.js @@ -1,4 +1,6 @@ import { CardSearchPagedComponent } from '../../lib/cardSearch'; +import {QueryParams} from "../../../config/query-classes"; +import {OPERATOR_SORT, OPERATOR_USER} from "../../../config/search-const"; // const subManager = new SubsManager(); @@ -48,10 +50,9 @@ class MyCardsComponent extends CardSearchPagedComponent { onCreated() { super.onCreated(); - const queryParams = { - users: [Meteor.user().username], - sort: { name: 'dueAt', order: 'des' }, - }; + const queryParams = new QueryParams(); + queryParams.addPredicate(OPERATOR_USER, Meteor.user().username); + queryParams.addPredicate(OPERATOR_SORT, { name: 'dueAt', order: 'des' }); this.runGlobalSearch(queryParams); Meteor.subscribe('setting'); diff --git a/config/query-classes.js b/config/query-classes.js new file mode 100644 index 000000000..f76109f45 --- /dev/null +++ b/config/query-classes.js @@ -0,0 +1,120 @@ +import { + OPERATOR_ASSIGNEE, + OPERATOR_BOARD, + OPERATOR_COMMENT, + OPERATOR_LABEL, + OPERATOR_LIST, + OPERATOR_MEMBER, + OPERATOR_SWIMLANE, + OPERATOR_USER, +} from './search-const'; + +export class QueryParams { + + text = ''; + + constructor(params = {}) { + this.params = params; + } + + hasOperator(operator) { + return this.params[operator]; + } + + addPredicate(operator, predicate) { + if (!this.hasOperator(operator)) { + this.params[operator] = []; + } + this.params[operator].push(predicate); + } + + setPredicate(operator, predicate) { + this.params[operator] = predicate; + } + + getPredicate(operator) { + return this.params[operator][0]; + } + + getPredicates(operator) { + return this.params[operator]; + } + + getParams() { + return this.params; + } +} + +export class QueryErrors { + constructor() { + this.errors = {}; + + this.colorMap = Boards.colorMap(); + } + + addError(operator, value) { + if (!this.errors[operator]) { + this.errors[operator] = []; + } + this.errors[operator].push(value) + } + + hasErrors() { + return Object.entries(this.errors).length > 0; + } + + errorMessages() { + const messages = []; + + const operatorTags = {}; + operatorTags[OPERATOR_BOARD] = 'board-title-not-found'; + operatorTags[OPERATOR_SWIMLANE] = 'swimlane-title-not-found'; + operatorTags[OPERATOR_LABEL] = label => { + if (Boards.labelColors().includes(label)) { + return { + tag: 'label-color-not-found', + value: label, + color: true, + }; + } else { + return { + tag: 'label-not-found', + value: label, + color: false, + }; + } + }; + operatorTags[OPERATOR_LIST] = 'list-title-not-found'; + operatorTags[OPERATOR_COMMENT] = 'comment-not-found'; + operatorTags[OPERATOR_USER] = 'user-username-not-found'; + operatorTags[OPERATOR_ASSIGNEE] = 'user-username-not-found'; + operatorTags[OPERATOR_MEMBER] = 'user-username-not-found'; + + Object.entries(this.errors, ([operator, value]) => { + if (typeof operatorTags[operator] === 'function') { + messages.push(operatorTags[operator](value)); + } else { + messages.push({ tag: operatorTags[operator], value: value }); + } + }); + + return messages; + } +} + +export class Query { + params = {}; + selector = {}; + projection = {}; + errors = new QueryErrors(); + + constructor(selector, projection) { + if (selector) { + this.selector = selector; + } + + if (projection) { + this.projection = projection; + } + } +} diff --git a/config/search-const.js b/config/search-const.js index 3bebf3490..fe16bee0e 100644 --- a/config/search-const.js +++ b/config/search-const.js @@ -1,7 +1,13 @@ +export const OPERATOR_ASSIGNEE = 'assignee'; +export const OPERATOR_COMMENT = 'comment'; export const OPERATOR_DUE = 'dueAt'; export const OPERATOR_BOARD = 'board'; +export const OPERATOR_HAS = 'has'; export const OPERATOR_LABEL = 'label'; export const OPERATOR_LIST = 'list'; +export const OPERATOR_MEMBER = 'member'; +export const OPERATOR_SORT = 'sort'; +export const OPERATOR_STATUS = 'status'; export const OPERATOR_SWIMLANE = 'swimlane'; export const OPERATOR_USER = 'user'; export const ORDER_ASCENDING = 'asc'; diff --git a/server/publications/cards.js b/server/publications/cards.js index 3c79a0405..fc226b538 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -9,8 +9,18 @@ import ChecklistItems from '../../models/checklistItems'; import SessionData from '../../models/usersessiondata'; import CustomFields from '../../models/customFields'; import { + OPERATOR_ASSIGNEE, OPERATOR_BOARD, - OPERATOR_DUE, OPERATOR_LIST, OPERATOR_SWIMLANE, OPERATOR_USER, + OPERATOR_COMMENT, + OPERATOR_DUE, + OPERATOR_HAS, + OPERATOR_LABEL, + OPERATOR_LIST, + OPERATOR_MEMBER, + OPERATOR_SORT, + OPERATOR_STATUS, + OPERATOR_SWIMLANE, + OPERATOR_USER, ORDER_ASCENDING, PREDICATE_ALL, PREDICATE_ARCHIVED, @@ -29,6 +39,7 @@ import { PREDICATE_START_AT, PREDICATE_SYSTEM, } from '../../config/search-const'; +import { QueryErrors, QueryParams, Query } from '../../config/query-classes'; const escapeForRegex = require('escape-string-regexp'); @@ -38,8 +49,8 @@ Meteor.publish('card', cardId => { }); Meteor.publish('myCards', function(sessionId) { - const queryParams = {} - queryParams[OPERATOR_USER] = [Meteor.user().username]; + const queryParams = new QueryParams(); + queryParams.addPredicate(OPERATOR_USER, Meteor.user().username); return findCards(sessionId, buildQuery(queryParams)); }); @@ -65,109 +76,16 @@ Meteor.publish('myCards', function(sessionId) { // return buildQuery(sessionId, queryParams); // }); -Meteor.publish('globalSearch', function(sessionId, queryParams) { +Meteor.publish('globalSearch', function(sessionId, params) { check(sessionId, String); - check(queryParams, Object); + check(params, Object); // eslint-disable-next-line no-console // console.log('queryParams:', queryParams); - return findCards(sessionId, buildQuery(queryParams)); + return findCards(sessionId, buildQuery(new QueryParams(params))); }); -class QueryErrors { - constructor() { - this.notFound = { - // boards: [], - // swimlanes: [], - // lists: [], - labels: [], - // users: [], - members: [], - assignees: [], - status: [], - comments: [], - }; - this.notFound[OPERATOR_BOARD] = []; - this.notFound[OPERATOR_LIST] = []; - this.notFound[OPERATOR_SWIMLANE] = []; - this.notFound[OPERATOR_USER] = []; - - this.colorMap = Boards.colorMap(); - } - - hasErrors() { - for (const value of Object.values(this.notFound)) { - if (value.length) { - return true; - } - } - return false; - } - - errorMessages() { - const messages = []; - - this.notFound[OPERATOR_BOARD].forEach(board => { - messages.push({ tag: 'board-title-not-found', value: board }); - }); - this.notFound[OPERATOR_SWIMLANE].forEach(swim => { - messages.push({ tag: 'swimlane-title-not-found', value: swim }); - }); - this.notFound[OPERATOR_LIST].forEach(list => { - messages.push({ tag: 'list-title-not-found', value: list }); - }); - this.notFound.comments.forEach(comments => { - comments.forEach(text => { - messages.push({ tag: 'comment-not-found', value: text }); - }); - }); - this.notFound.labels.forEach(label => { - if (Boards.labelColors().includes(label)) { - messages.push({ - tag: 'label-color-not-found', - value: label, - color: true, - }); - } else { - messages.push({ - tag: 'label-not-found', - value: label, - color: false, - }); - } - }); - this.notFound[OPERATOR_USER].forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - this.notFound.members.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - this.notFound.assignees.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - - return messages; - } -} - -class Query { - params = {}; - selector = {}; - projection = {}; - errors = new QueryErrors(); - - constructor(selector, projection) { - if (selector) { - this.selector = selector; - } - - if (projection) { - this.projection = projection; - } - } -} - function buildSelector(queryParams) { const userId = Meteor.userId(); @@ -182,8 +100,8 @@ function buildSelector(queryParams) { let archived = false; let endAt = null; - if (queryParams.status && queryParams.status.length) { - queryParams.status.forEach(status => { + if (queryParams.hasOperator(OPERATOR_STATUS)) { + queryParams.getPredicates(OPERATOR_STATUS).forEach(status => { if (status === PREDICATE_ARCHIVED) { archived = true; } else if (status === PREDICATE_ALL) { @@ -235,9 +153,9 @@ function buildSelector(queryParams) { selector.endAt = endAt; } - if (queryParams[OPERATOR_BOARD] && queryParams[OPERATOR_BOARD].length) { + if (queryParams.hasOperator(OPERATOR_BOARD)) { const queryBoards = []; - queryParams[OPERATOR_BOARD].forEach(query => { + queryParams.hasOperator(OPERATOR_BOARD).forEach(query => { const boards = Boards.userSearch(userId, { title: new RegExp(escapeForRegex(query), 'i'), }); @@ -246,19 +164,16 @@ function buildSelector(queryParams) { queryBoards.push(board._id); }); } else { - errors.notFound[OPERATOR_BOARD].push(query); + errors.addError(OPERATOR_BOARD, query); } }); selector.boardId.$in = queryBoards; } - if ( - queryParams[OPERATOR_SWIMLANE] && - queryParams[OPERATOR_SWIMLANE].length - ) { + if (queryParams.hasOperator(OPERATOR_SWIMLANE)) { const querySwimlanes = []; - queryParams[OPERATOR_SWIMLANE].forEach(query => { + queryParams.getPredicates(OPERATOR_SWIMLANE).forEach(query => { const swimlanes = Swimlanes.find({ title: new RegExp(escapeForRegex(query), 'i'), }); @@ -267,7 +182,7 @@ function buildSelector(queryParams) { querySwimlanes.push(swim._id); }); } else { - errors.notFound[OPERATOR_SWIMLANE].push(query); + errors.addError(OPERATOR_SWIMLANE, query); } }); @@ -278,9 +193,9 @@ function buildSelector(queryParams) { selector.swimlaneId.$in = querySwimlanes; } - if (queryParams[OPERATOR_LIST] && queryParams[OPERATOR_LIST].length) { + if (queryParams.hasOperator(OPERATOR_LIST)) { const queryLists = []; - queryParams[OPERATOR_LIST].forEach(query => { + queryParams.getPredicates(OPERATOR_LIST).forEach(query => { const lists = Lists.find({ title: new RegExp(escapeForRegex(query), 'i'), }); @@ -289,7 +204,7 @@ function buildSelector(queryParams) { queryLists.push(list._id); }); } else { - errors.notFound[OPERATOR_LIST].push(query); + errors.addError(OPERATOR_LIST, query); } }); @@ -300,8 +215,8 @@ function buildSelector(queryParams) { selector.listId.$in = queryLists; } - if (queryParams.comments && queryParams.comments.length) { - const cardIds = CardComments.textSearch(userId, queryParams.comments).map( + if (queryParams.hasOperator(OPERATOR_COMMENT)) { + const cardIds = CardComments.textSearch(userId, queryParams.getPredicates(OPERATOR_COMMENT)).map( com => { return com.cardId; }, @@ -309,42 +224,43 @@ function buildSelector(queryParams) { if (cardIds.length) { selector._id = { $in: cardIds }; } else { - errors.notFound.comments.push(queryParams.comments); + queryParams.getPredicates(OPERATOR_COMMENT).forEach(comment => { + errors.addError(OPERATOR_COMMENT, comment); + }); } } [OPERATOR_DUE, 'createdAt', 'modifiedAt'].forEach(field => { - if (queryParams[field]) { + if (queryParams.hasOperator(field)) { selector[field] = {}; - selector[field][queryParams[field].operator] = new Date( - queryParams[field].value, - ); + const predicate = queryParams.getPredicate(field); + selector[field][predicate.operator] = new Date(predicate.value); } }); - const queryUsers = { - members: [], - assignees: [], - }; - if (queryParams[OPERATOR_USER] && queryParams[OPERATOR_USER].length) { - queryParams[OPERATOR_USER].forEach(query => { + const queryUsers = {} + queryUsers[OPERATOR_ASSIGNEE] = []; + queryUsers[OPERATOR_MEMBER] = []; + + if (queryParams.hasOperator(OPERATOR_USER)) { + queryParams.getPredicates(OPERATOR_USER).forEach(query => { const users = Users.find({ username: query, }); if (users.count()) { users.forEach(user => { - queryUsers.members.push(user._id); - queryUsers.assignees.push(user._id); + queryUsers[OPERATOR_MEMBER].push(user._id); + queryUsers[OPERATOR_ASSIGNEE].push(user._id); }); } else { - errors.notFound[OPERATOR_USER].push(query); + errors.addError(OPERATOR_USER, query); } }); } - ['members', 'assignees'].forEach(key => { - if (queryParams[key] && queryParams[key].length) { - queryParams[key].forEach(query => { + [OPERATOR_MEMBER, OPERATOR_ASSIGNEE].forEach(key => { + if (queryParams.hasOperator(key)) { + queryParams.getPredicates(key).forEach(query => { const users = Users.find({ username: query, }); @@ -353,27 +269,27 @@ function buildSelector(queryParams) { queryUsers[key].push(user._id); }); } else { - errors.notFound[key].push(query); + errors.addError(key, query); } }); } }); - if (queryUsers.members.length && queryUsers.assignees.length) { + if (queryUsers[OPERATOR_MEMBER].length && queryUsers[OPERATOR_ASSIGNEE].length) { selector.$and.push({ $or: [ - { members: { $in: queryUsers.members } }, - { assignees: { $in: queryUsers.assignees } }, + { members: { $in: queryUsers[OPERATOR_MEMBER] } }, + { assignees: { $in: queryUsers[OPERATOR_ASSIGNEE] } }, ], }); - } else if (queryUsers.members.length) { - selector.members = { $in: queryUsers.members }; - } else if (queryUsers.assignees.length) { - selector.assignees = { $in: queryUsers.assignees }; + } else if (queryUsers[OPERATOR_MEMBER].length) { + selector.members = { $in: queryUsers[OPERATOR_MEMBER] }; + } else if (queryUsers[OPERATOR_ASSIGNEE].length) { + selector.assignees = { $in: queryUsers[OPERATOR_ASSIGNEE] }; } - if (queryParams.labels && queryParams.labels.length) { - queryParams.labels.forEach(label => { + if (queryParams.hasOperator(OPERATOR_LABEL)) { + queryParams.getPredicates(OPERATOR_LABEL).forEach(label => { const queryLabels = []; let boards = Boards.userSearch(userId, { @@ -418,7 +334,7 @@ function buildSelector(queryParams) { }); }); } else { - errors.notFound.labels.push(label); + errors.addError(OPERATOR_LABEL, label); } } @@ -426,8 +342,8 @@ function buildSelector(queryParams) { }); } - if (queryParams.has && queryParams.has.length) { - queryParams.has.forEach(has => { + if (queryParams.hasOperator(OPERATOR_HAS)) { + queryParams.getPredicates(OPERATOR_HAS).forEach(has => { switch (has.field) { case PREDICATE_ATTACHMENT: selector.$and.push({ @@ -569,9 +485,9 @@ function buildProjection(query) { limit, }; - if (query.params.sort) { - const order = query.params.sort.order === ORDER_ASCENDING ? 1 : -1; - switch (query.params.sort.name) { + if (query.params[OPERATOR_SORT]) { + const order = query.params[OPERATOR_SORT].order === ORDER_ASCENDING ? 1 : -1; + switch (query.params[OPERATOR_SORT].name) { case PREDICATE_DUE_AT: projection.sort = { dueAt: order, @@ -628,7 +544,9 @@ function buildQuery(queryParams) { Meteor.publish('brokenCards', function(sessionId) { check(sessionId, String); - const query = buildQuery({ status: [PREDICATE_ALL] }); + const params = new QueryParams(); + params.addPredicate(OPERATOR_STATUS, PREDICATE_ALL); + const query = buildQuery(params); query.selector.$or = [ { boardId: { $in: [null, ''] } }, { swimlaneId: { $in: [null, ''] } },