From a1bda1169e1c5e02081a063e147b9b8bfdabbfe5 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Mon, 1 Mar 2021 01:49:56 +0200 Subject: [PATCH 01/17] Update Global Search, Due Cards, and My Cards to use the same code for searching and display --- client/components/main/dueCards.jade | 16 +- client/components/main/dueCards.js | 117 +++-------- client/components/main/globalSearch.jade | 33 +-- client/components/main/globalSearch.js | 234 ++++----------------- client/components/main/myCards.jade | 8 +- client/components/main/myCards.js | 249 +++++++++-------------- server/publications/cards.js | 246 ++++++---------------- 7 files changed, 266 insertions(+), 637 deletions(-) diff --git a/client/components/main/dueCards.jade b/client/components/main/dueCards.jade index 2570984de..a1970839e 100644 --- a/client/components/main/dueCards.jade +++ b/client/components/main/dueCards.jade @@ -22,13 +22,17 @@ template(name="dueCardsModalTitle") template(name="dueCards") if currentUser - if isPageReady.get - .wrapper - .due-cards-dueat-list-wrapper - each card in dueCardsList - +resultCard(card) - else + if searching.get +spinner + else if hasResults.get + .global-search-results-list-wrapper + if hasQueryErrors.get + div + each msg in errorMessages + span.global-search-error-messages + = msg + else + +resultsPaged(this) template(name="dueCardsViewChangePopup") if currentUser diff --git a/client/components/main/dueCards.js b/client/components/main/dueCards.js index 747c4f457..ac3ab3164 100644 --- a/client/components/main/dueCards.js +++ b/client/components/main/dueCards.js @@ -1,3 +1,5 @@ +import { CardSearchPagedComponent } from '../../lib/cardSearch'; + const subManager = new SubsManager(); BlazeComponent.extendComponent({ @@ -40,102 +42,43 @@ BlazeComponent.extendComponent({ }, }).register('dueCardsViewChangePopup'); -BlazeComponent.extendComponent({ +class DueCardsComponent extends CardSearchPagedComponent { onCreated() { - this.isPageReady = new ReactiveVar(false); + super.onCreated(); - this.autorun(() => { - const handle = subManager.subscribe( - 'dueCards', - Utils.dueCardsView() === 'all', - ); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - this.isPageReady.set(handle.ready()); - }); - }); - }); - Meteor.subscribe('setting'); - }, + const queryParams = { + has: [{ field: 'dueAt', exists: true }], + limit: 5, + skip: 0, + sort: { name: 'dueAt', order: 'des' }, + }; + + if (Utils.dueCardsView() !== 'all') { + queryParams.users = [Meteor.user().username]; + } + + this.autorunGlobalSearch(queryParams); + } dueCardsView() { // eslint-disable-next-line no-console //console.log('sort:', Utils.dueCardsView()); return Utils.dueCardsView(); - }, + } sortByBoard() { return this.dueCardsView() === 'board'; - }, + } dueCardsList() { - const allUsers = Utils.dueCardsView() === 'all'; - - const user = Meteor.user(); - - const archivedBoards = []; - Boards.find({ archived: true }).forEach(board => { - archivedBoards.push(board._id); - }); - - const permiitedBoards = []; - let selector = { - archived: false, - }; - // for every user including admin allow her to see cards only from public boards - // or those where she is a member - //if (!user.isAdmin) { - selector.$or = [ - { permission: 'public' }, - { members: { $elemMatch: { userId: user._id, isActive: true } } }, - ]; - //} - Boards.find(selector).forEach(board => { - permiitedBoards.push(board._id); - }); - - const archivedSwimlanes = []; - Swimlanes.find({ archived: true }).forEach(swimlane => { - archivedSwimlanes.push(swimlane._id); - }); - - const archivedLists = []; - Lists.find({ archived: true }).forEach(list => { - archivedLists.push(list._id); - }); - - selector = { - archived: false, - boardId: { - $nin: archivedBoards, - $in: permiitedBoards, - }, - swimlaneId: { $nin: archivedSwimlanes }, - listId: { $nin: archivedLists }, - dueAt: { $ne: null }, - endAt: null, - }; - - if (!allUsers) { - selector.$or = [{ members: user._id }, { assignees: user._id }]; - } - + const results = this.getResults(); + console.log('results:', results); const cards = []; - - // eslint-disable-next-line no-console - // console.log('cards selector:', selector); - Cards.find(selector).forEach(card => { - cards.push(card); - // eslint-disable-next-line no-console - // console.log( - // 'board:', - // card.board(), - // 'swimlane:', - // card.swimlane(), - // 'list:', - // card.list(), - // ); - }); + if (results) { + results.forEach(card => { + cards.push(card); + }); + } cards.sort((a, b) => { const x = a.dueAt === null ? Date('2100-12-31') : a.dueAt; @@ -148,7 +91,9 @@ BlazeComponent.extendComponent({ }); // eslint-disable-next-line no-console - // console.log('cards:', cards); + console.log('cards:', cards); return cards; - }, -}).register('dueCards'); + } +} + +DueCardsComponent.register('dueCards'); diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 989075d02..457461965 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -10,6 +10,23 @@ template(name="globalSearchModalTitle") i.fa.fa-keyboard-o | {{_ 'globalSearch-title'}} +template(name="resultsPaged") + h1 + = resultsHeading.get + a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}") + each card in results.get + +resultCard(card) + table.global-search-footer + tr + td.global-search-previous-page + if hasPreviousPage.get + button.js-previous-page + | {{_ 'previous-page' }} + td.global-search-next-page(align="right") + if hasNextPage.get + button.js-next-page + | {{_ 'next-page' }} + template(name="globalSearch") if currentUser .wrapper @@ -32,21 +49,7 @@ template(name="globalSearch") span.global-search-error-messages = msg else - h1 - = resultsHeading.get - a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}") - each card in results.get - +resultCard(card) - table.global-search-footer - tr - td.global-search-previous-page - if hasPreviousPage.get - button.js-previous-page - | {{_ 'previous-page' }} - td.global-search-next-page(align="right") - if hasNextPage.get - button.js-next-page - | {{_ 'next-page' }} + +resultsPaged(this) else .global-search-page .global-search-help diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 7a307b10a..217ff6daa 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -1,4 +1,6 @@ -const subManager = new SubsManager(); +import { CardSearchPagedComponent } from '../../lib/cardSearch'; + +// const subManager = new SubsManager(); BlazeComponent.extendComponent({ events() { @@ -34,27 +36,15 @@ BlazeComponent.extendComponent({ }, }).register('globalSearchViewChangePopup'); -BlazeComponent.extendComponent({ +class GlobalSearchComponent extends CardSearchPagedComponent { onCreated() { - this.searching = new ReactiveVar(false); - this.hasResults = new ReactiveVar(false); - this.hasQueryErrors = new ReactiveVar(false); - this.query = new ReactiveVar(''); - this.resultsHeading = new ReactiveVar(''); - this.searchLink = new ReactiveVar(null); + super.onCreated(); this.myLists = new ReactiveVar([]); this.myLabelNames = new ReactiveVar([]); this.myBoardNames = new ReactiveVar([]); - this.results = new ReactiveVar([]); - this.hasNextPage = new ReactiveVar(false); - this.hasPreviousPage = new ReactiveVar(false); - this.queryParams = null; this.parsingErrors = []; - this.resultsCount = 0; - this.totalHits = 0; - this.queryErrors = null; this.colorMap = null; - this.resultsPerPage = 25; + this.queryParams = null; Meteor.call('myLists', (err, data) => { if (!err) { @@ -73,7 +63,7 @@ BlazeComponent.extendComponent({ this.myBoardNames.set(data); } }); - }, + } onRendered() { Meteor.subscribe('setting'); @@ -87,67 +77,19 @@ BlazeComponent.extendComponent({ if (Session.get('globalQuery')) { this.searchAllBoards(Session.get('globalQuery')); } - }, + } resetSearch() { - this.searching.set(false); - this.results.set([]); - this.hasResults.set(false); - this.hasQueryErrors.set(false); - this.resultsHeading.set(''); + super.resetSearch(); this.parsingErrors = []; - this.resultsCount = 0; - this.totalHits = 0; - this.queryErrors = null; - }, - - getSessionData() { - return SessionData.findOne({ - userId: Meteor.userId(), - sessionId: SessionData.getSessionId(), - }); - }, - - getResults() { - // eslint-disable-next-line no-console - // console.log('getting results'); - if (this.queryParams) { - const sessionData = this.getSessionData(); - // eslint-disable-next-line no-console - // console.log('selector:', sessionData.getSelector()); - // console.log('session data:', sessionData); - const projection = sessionData.getProjection(); - projection.skip = 0; - const cards = Cards.find({ _id: { $in: sessionData.cards } }, projection); - this.queryErrors = sessionData.errors; - if (this.queryErrors.length) { - this.hasQueryErrors.set(true); - return null; - } - - if (cards) { - this.totalHits = sessionData.totalHits; - this.resultsCount = cards.count(); - this.resultsStart = sessionData.lastHit - this.resultsCount + 1; - this.resultsEnd = sessionData.lastHit; - this.resultsHeading.set(this.getResultsHeading()); - this.results.set(cards); - this.hasNextPage.set(sessionData.lastHit < sessionData.totalHits); - this.hasPreviousPage.set( - sessionData.lastHit - sessionData.resultsCount > 0, - ); - } - } - this.resultsCount = 0; - return null; - }, + } errorMessages() { if (this.parsingErrors.length) { return this.parsingErrorMessages(); } return this.queryErrorMessages(); - }, + } parsingErrorMessages() { const messages = []; @@ -159,21 +101,7 @@ BlazeComponent.extendComponent({ } return messages; - }, - - queryErrorMessages() { - messages = []; - - this.queryErrors.forEach(err => { - let value = err.color ? TAPi18n.__(`color-${err.value}`) : err.value; - if (!value) { - value = err.value; - } - messages.push(TAPi18n.__(err.tag, value)); - }); - - return messages; - }, + } searchAllBoards(query) { query = query.trim(); @@ -300,7 +228,7 @@ BlazeComponent.extendComponent({ let text = ''; while (query) { - m = query.match(reOperator1); + let m = query.match(reOperator1); if (!m) { m = query.match(reOperator2); if (m) { @@ -326,7 +254,7 @@ BlazeComponent.extendComponent({ // console.log('found color:', value); } } else if (['dueAt', 'createdAt', 'modifiedAt'].includes(operator)) { - let days = parseInt(value, 10); + const days = parseInt(value, 10); let duration = null; if (isNaN(days)) { // duration was specified as text @@ -335,7 +263,8 @@ BlazeComponent.extendComponent({ let date = null; switch (duration) { case 'week': - let week = moment().week(); + // eslint-disable-next-line no-case-declarations + const week = moment().week(); if (week === 52) { date = moment(1, 'W'); date.set('year', date.year() + 1); @@ -344,7 +273,8 @@ BlazeComponent.extendComponent({ } break; case 'month': - let month = moment().month(); + // eslint-disable-next-line no-case-declarations + const month = moment().month(); // .month() is zero indexed if (month === 11) { date = moment(1, 'M'); @@ -354,7 +284,8 @@ BlazeComponent.extendComponent({ } break; case 'quarter': - let quarter = moment().quarter(); + // eslint-disable-next-line no-case-declarations + const quarter = moment().quarter(); if (quarter === 4) { date = moment(1, 'Q'); date.set('year', date.year() + 1); @@ -384,22 +315,20 @@ BlazeComponent.extendComponent({ }); value = null; } + } else if (operator === 'dueAt') { + value = { + operator: '$lt', + value: moment(moment().format('YYYY-MM-DD')) + .add(days + 1, duration ? duration : 'days') + .format(), + }; } else { - if (operator === 'dueAt') { - value = { - operator: '$lt', - value: moment(moment().format('YYYY-MM-DD')) - .add(days + 1, duration ? duration : 'days') - .format(), - }; - } else { - value = { - operator: '$gte', - value: moment(moment().format('YYYY-MM-DD')) - .subtract(days, duration ? duration : 'days') - .format(), - }; - } + value = { + operator: '$gte', + value: moment(moment().format('YYYY-MM-DD')) + .subtract(days, duration ? duration : 'days') + .format(), + }; } } else if (operator === 'sort') { let negated = false; @@ -502,81 +431,11 @@ BlazeComponent.extendComponent({ return; } - this.autorun(() => { - const handle = Meteor.subscribe( - 'globalSearch', - SessionData.getSessionId(), - params, - ); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - if (handle.ready()) { - this.getResults(); - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); - }, - - nextPage() { - const sessionData = this.getSessionData(); - - this.autorun(() => { - const handle = Meteor.subscribe('nextPage', sessionData.sessionId); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - if (handle.ready()) { - this.getResults(); - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); - }, - - previousPage() { - const sessionData = this.getSessionData(); - - this.autorun(() => { - const handle = Meteor.subscribe('previousPage', sessionData.sessionId); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - if (handle.ready()) { - this.getResults(); - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); - }, - - getResultsHeading() { - if (this.resultsCount === 0) { - return TAPi18n.__('no-cards-found'); - } else if (this.resultsCount === 1) { - return TAPi18n.__('one-card-found'); - } else if (this.resultsCount === this.totalHits) { - return TAPi18n.__('n-cards-found', this.resultsCount); - } - - return TAPi18n.__('n-n-of-n-cards-found', { - start: this.resultsStart, - end: this.resultsEnd, - total: this.totalHits, - }); - }, - - getSearchHref() { - const baseUrl = window.location.href.replace(/([?#].*$|\s*$)/, ''); - return `${baseUrl}?q=${encodeURIComponent(this.query.get())}`; - }, + this.autorunGlobalSearch(params); + } searchInstructions() { - tags = { + const tags = { operator_board: TAPi18n.__('operator-board'), operator_list: TAPi18n.__('operator-list'), operator_swimlane: TAPi18n.__('operator-swimlane'), @@ -654,7 +513,7 @@ BlazeComponent.extendComponent({ [ 'globalSearch-instructions-operator-has', 'globalSearch-instructions-operator-sort', - 'globalSearch-instructions-operator-limit' + 'globalSearch-instructions-operator-limit', ].forEach(instruction => { text += `\n* ${TAPi18n.__(instruction, tags)}`; }); @@ -672,7 +531,7 @@ BlazeComponent.extendComponent({ }); return text; - }, + } labelColors() { return Boards.simpleSchema()._schema['labels.$.color'].allowedValues.map( @@ -680,23 +539,16 @@ BlazeComponent.extendComponent({ return { color, name: TAPi18n.__(`color-${color}`) }; }, ); - }, + } events() { return [ { + ...super.events()[0], 'submit .js-search-query-form'(evt) { evt.preventDefault(); this.searchAllBoards(evt.target.searchQuery.value); }, - 'click .js-next-page'(evt) { - evt.preventDefault(); - this.nextPage(); - }, - 'click .js-previous-page'(evt) { - evt.preventDefault(); - this.previousPage(); - }, 'click .js-label-color'(evt) { evt.preventDefault(); const input = document.getElementById('global-search-input'); @@ -739,5 +591,7 @@ BlazeComponent.extendComponent({ }, }, ]; - }, -}).register('globalSearch'); + } +} + +GlobalSearchComponent.register('globalSearch'); diff --git a/client/components/main/myCards.jade b/client/components/main/myCards.jade index 79d28050b..3daae780b 100644 --- a/client/components/main/myCards.jade +++ b/client/components/main/myCards.jade @@ -24,14 +24,16 @@ template(name="myCardsModalTitle") template(name="myCards") if currentUser - if isPageReady.get + if searching.get + +spinner + else .wrapper if $eq myCardsSort 'board' each board in myCardsList .my-cards-board-wrapper .my-cards-board-title(class=board.colorClass, id="header") a(href=board.absoluteUrl) - +viewer + +viewer = board.title each swimlane in board.mySwimlanes .my-cards-swimlane-title(class="{{#if swimlane.colorClass}}{{ swimlane.colorClass }}{{else}}swimlane-default-color{{/if}}") @@ -50,8 +52,6 @@ template(name="myCards") .my-cards-dueat-list-wrapper each card in myDueCardsList +resultCard(card) - else - +spinner template(name="myCardsSortChangePopup") if currentUser diff --git a/client/components/main/myCards.js b/client/components/main/myCards.js index 13eb3e519..54aa8f505 100644 --- a/client/components/main/myCards.js +++ b/client/components/main/myCards.js @@ -1,3 +1,5 @@ +import {CardSearchPagedComponent} from "../../lib/cardSearch"; + const subManager = new SubsManager(); BlazeComponent.extendComponent({ @@ -42,177 +44,139 @@ BlazeComponent.extendComponent({ }, }).register('myCardsSortChangePopup'); -BlazeComponent.extendComponent({ +class MyCardsComponent extends CardSearchPagedComponent { onCreated() { - this.isPageReady = new ReactiveVar(false); + super.onCreated(); - this.autorun(() => { - const handle = subManager.subscribe('myCards'); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - this.isPageReady.set(handle.ready()); - }); - }); - }); + const queryParams = { + users: [Meteor.user().username], + sort: { name: 'dueAt', order: 'des' }, + }; + + this.autorunGlobalSearch(queryParams); Meteor.subscribe('setting'); - }, + } myCardsSort() { // eslint-disable-next-line no-console //console.log('sort:', Utils.myCardsSort()); return Utils.myCardsSort(); - }, + } sortByBoard() { return this.myCardsSort() === 'board'; - }, + } myCardsList() { - const userId = Meteor.userId(); const boards = []; let board = null; let swimlane = null; let list = null; - const cursor = Cards.find( - { - $or: [{ members: userId }, { assignees: userId }], - archived: false, - }, - { - sort: { - boardId: 1, - swimlaneId: 1, - listId: 1, - sort: 1, - }, - }, - ); + const cursor = this.getResults(); - let newBoard = false; - let newSwimlane = false; - let newList = false; + if (cursor) { + let newBoard = false; + let newSwimlane = false; + let newList = false; - cursor.forEach(card => { - // eslint-disable-next-line no-console - // console.log('card:', card.title); - if (list === null || card.listId !== list._id) { + cursor.forEach(card => { // eslint-disable-next-line no-console - // console.log('new list'); - list = card.getList(); - if (list.archived) { - list = null; - return; + // console.log('card:', card.title); + if (list === null || card.listId !== list._id) { + // eslint-disable-next-line no-console + // console.log('new list'); + list = card.getList(); + if (list.archived) { + list = null; + return; + } + list.myCards = [card]; + newList = true; } - list.myCards = [card]; - newList = true; - } - if (swimlane === null || card.swimlaneId !== swimlane._id) { - // eslint-disable-next-line no-console - // console.log('new swimlane'); - swimlane = card.getSwimlane(); - if (swimlane.archived) { - swimlane = null; - return; + if (swimlane === null || card.swimlaneId !== swimlane._id) { + // eslint-disable-next-line no-console + // console.log('new swimlane'); + swimlane = card.getSwimlane(); + if (swimlane.archived) { + swimlane = null; + return; + } + swimlane.myLists = [list]; + newSwimlane = true; } - swimlane.myLists = [list]; - newSwimlane = true; - } - if (board === null || card.boardId !== board._id) { - // eslint-disable-next-line no-console - // console.log('new board'); - board = card.getBoard(); - if (board.archived) { - board = null; - return; + if (board === null || card.boardId !== board._id) { + // eslint-disable-next-line no-console + // console.log('new board'); + board = card.getBoard(); + if (board.archived) { + board = null; + return; + } + // eslint-disable-next-line no-console + // console.log('board:', b, b._id, b.title); + board.mySwimlanes = [swimlane]; + newBoard = true; } - // eslint-disable-next-line no-console - // console.log('board:', b, b._id, b.title); - board.mySwimlanes = [swimlane]; - newBoard = true; - } - if (newBoard) { - boards.push(board); - } else if (newSwimlane) { - board.mySwimlanes.push(swimlane); - } else if (newList) { - swimlane.myLists.push(list); - } else { - list.myCards.push(card); - } + if (newBoard) { + boards.push(board); + } else if (newSwimlane) { + board.mySwimlanes.push(swimlane); + } else if (newList) { + swimlane.myLists.push(list); + } else { + list.myCards.push(card); + } - newBoard = false; - newSwimlane = false; - newList = false; - }); + newBoard = false; + newSwimlane = false; + newList = false; + }); - // sort the data structure - boards.forEach(board => { - board.mySwimlanes.forEach(swimlane => { - swimlane.myLists.forEach(list => { - list.myCards.sort((a, b) => { + // sort the data structure + boards.forEach(board => { + board.mySwimlanes.forEach(swimlane => { + swimlane.myLists.forEach(list => { + list.myCards.sort((a, b) => { + return a.sort - b.sort; + }); + }); + swimlane.myLists.sort((a, b) => { return a.sort - b.sort; }); }); - swimlane.myLists.sort((a, b) => { + board.mySwimlanes.sort((a, b) => { return a.sort - b.sort; }); }); - board.mySwimlanes.sort((a, b) => { - return a.sort - b.sort; + + boards.sort((a, b) => { + let x = a.sort; + let y = b.sort; + + // show the template board last + if (a.type === 'template-container') { + x = 99999999; + } else if (b.type === 'template-container') { + y = 99999999; + } + return x - y; }); - }); - boards.sort((a, b) => { - let x = a.sort; - let y = b.sort; + // eslint-disable-next-line no-console + // console.log('boards:', boards); + return boards; + } - // show the template board last - if (a.type === 'template-container') { - x = 99999999; - } else if (b.type === 'template-container') { - y = 99999999; - } - return x - y; - }); - - // eslint-disable-next-line no-console - // console.log('boards:', boards); - return boards; - }, + return []; + } myDueCardsList() { - const userId = Meteor.userId(); - - const cursor = Cards.find( - { - $or: [{ members: userId }, { assignees: userId }], - archived: false, - }, - { - sort: { - dueAt: -1, - boardId: 1, - swimlaneId: 1, - listId: 1, - sort: 1, - }, - }, - ); - - // eslint-disable-next-line no-console - // console.log('cursor:', cursor); - + const cursor = this.getResults(); const cards = []; cursor.forEach(card => { - if ( - !card.getBoard().archived && - !card.getSwimlane().archived && - !card.getList().archived - ) { - cards.push(card); - } + cards.push(card); }); cards.sort((a, b) => { @@ -228,23 +192,6 @@ BlazeComponent.extendComponent({ // eslint-disable-next-line no-console // console.log('cursor:', cards); return cards; - }, - - events() { - return [ - { - // 'click .js-my-card'(evt) { - // const card = this.currentData().card; - // // eslint-disable-next-line no-console - // console.log('currentData():', this.currentData()); - // // eslint-disable-next-line no-console - // console.log('card:', card); - // if (card) { - // Utils.goCardId(card._id); - // } - // evt.preventDefault(); - // }, - }, - ]; - }, -}).register('myCards'); + } +} +MyCardsComponent.register('myCards'); diff --git a/server/publications/cards.js b/server/publications/cards.js index 4197fd826..a7b3f699c 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -5,175 +5,38 @@ Meteor.publish('card', cardId => { return Cards.find({ _id: cardId }); }); -Meteor.publish('myCards', function() { - const userId = Meteor.userId(); +Meteor.publish('myCards', function(sessionId) { - const archivedBoards = []; - Boards.find({ archived: true }).forEach(board => { - archivedBoards.push(board._id); - }); - - const archivedSwimlanes = []; - Swimlanes.find({ archived: true }).forEach(swimlane => { - archivedSwimlanes.push(swimlane._id); - }); - - const archivedLists = []; - Lists.find({ archived: true }).forEach(list => { - archivedLists.push(list._id); - }); - - selector = { - archived: false, - boardId: { $nin: archivedBoards }, - swimlaneId: { $nin: archivedSwimlanes }, - listId: { $nin: archivedLists }, - $or: [{ members: userId }, { assignees: userId }], + const queryParams = { + users: [Meteor.user().username], + // limit: 25, + skip: 0, + // sort: { name: 'dueAt', order: 'des' }, }; - 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 buildQuery(sessionId, queryParams); }); -Meteor.publish('dueCards', function(allUsers = false) { - check(allUsers, Boolean); - - // eslint-disable-next-line no-console - // console.log('all users:', allUsers); - - const user = Users.findOne({ _id: this.userId }); - - const archivedBoards = []; - Boards.find({ archived: true }).forEach(board => { - archivedBoards.push(board._id); - }); - - const permiitedBoards = []; - let selector = { - archived: false, - }; - - selector.$or = [ - { permission: 'public' }, - { members: { $elemMatch: { userId: user._id, isActive: true } } }, - ]; - - Boards.find(selector).forEach(board => { - permiitedBoards.push(board._id); - }); - - const archivedSwimlanes = []; - Swimlanes.find({ archived: true }).forEach(swimlane => { - archivedSwimlanes.push(swimlane._id); - }); - - const archivedLists = []; - Lists.find({ archived: true }).forEach(list => { - archivedLists.push(list._id); - }); - - selector = { - archived: false, - boardId: { $nin: archivedBoards, $in: permiitedBoards }, - swimlaneId: { $nin: archivedSwimlanes }, - listId: { $nin: archivedLists }, - dueAt: { $ne: null }, - endAt: null, - }; - - if (!allUsers) { - selector.$or = [{ members: user._id }, { assignees: user._id }]; - } - - const cards = Cards.find(selector, { - fields: { - _id: 1, - archived: 1, - boardId: 1, - swimlaneId: 1, - listId: 1, - title: 1, - type: 1, - sort: 1, - members: 1, - assignees: 1, - colors: 1, - dueAt: 1, - }, - }); - - const boards = []; - const swimlanes = []; - const lists = []; - const users = []; - - cards.forEach(card => { - if (card.boardId) boards.push(card.boardId); - if (card.swimlaneId) swimlanes.push(card.swimlaneId); - if (card.listId) lists.push(card.listId); - if (card.members) { - card.members.forEach(userId => { - users.push(userId); - }); - } - if (card.assignees) { - card.assignees.forEach(userId => { - users.push(userId); - }); - } - }); - - return [ - cards, - Boards.find({ _id: { $in: boards } }), - Swimlanes.find({ _id: { $in: swimlanes } }), - Lists.find({ _id: { $in: lists } }), - Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), - ]; -}); +// Meteor.publish('dueCards', function(sessionId, allUsers = false) { +// check(sessionId, String); +// check(allUsers, Boolean); +// +// // eslint-disable-next-line no-console +// // console.log('all users:', allUsers); +// +// const queryParams = { +// has: [{ field: 'dueAt', exists: true }], +// limit: 25, +// skip: 0, +// sort: { name: 'dueAt', order: 'des' }, +// }; +// +// if (!allUsers) { +// queryParams.users = [Meteor.user().username]; +// } +// +// return buildQuery(sessionId, queryParams); +// }); Meteor.publish('globalSearch', function(sessionId, queryParams) { check(sessionId, String); @@ -182,9 +45,11 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { // eslint-disable-next-line no-console // console.log('queryParams:', queryParams); + return buildQuery(sessionId, queryParams); +}); + +function buildQuery(sessionId, queryParams) { const userId = Meteor.userId(); - // eslint-disable-next-line no-console - // console.log('userId:', userId); const errors = new (class { constructor() { @@ -267,7 +132,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { let archived = false; let endAt = null; - if (queryParams.status.length) { + if (queryParams.status && queryParams.status.length) { queryParams.status.forEach(status => { if (status === 'archived') { archived = true; @@ -320,7 +185,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { selector.endAt = endAt; } - if (queryParams.boards.length) { + if (queryParams.boards && queryParams.boards.length) { const queryBoards = []; queryParams.boards.forEach(query => { const boards = Boards.userSearch(userId, { @@ -338,7 +203,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { selector.boardId.$in = queryBoards; } - if (queryParams.swimlanes.length) { + if (queryParams.swimlanes && queryParams.swimlanes.length) { const querySwimlanes = []; queryParams.swimlanes.forEach(query => { const swimlanes = Swimlanes.find({ @@ -360,7 +225,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { selector.swimlaneId.$in = querySwimlanes; } - if (queryParams.lists.length) { + if (queryParams.lists && queryParams.lists.length) { const queryLists = []; queryParams.lists.forEach(query => { const lists = Lists.find({ @@ -382,7 +247,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { selector.listId.$in = queryLists; } - if (queryParams.comments.length) { + if (queryParams.comments && queryParams.comments.length) { const cardIds = CardComments.textSearch(userId, queryParams.comments).map( com => { return com.cardId; @@ -398,15 +263,15 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { ['dueAt', 'createdAt', 'modifiedAt'].forEach(field => { if (queryParams[field]) { selector[field] = {}; - selector[field][queryParams[field]['operator']] = new Date( - queryParams[field]['value'], + selector[field][queryParams[field].operator] = new Date( + queryParams[field].value, ); } }); const queryMembers = []; const queryAssignees = []; - if (queryParams.users.length) { + if (queryParams.users && queryParams.users.length) { queryParams.users.forEach(query => { const users = Users.find({ username: query, @@ -422,7 +287,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { }); } - if (queryParams.members.length) { + if (queryParams.members && queryParams.members.length) { queryParams.members.forEach(query => { const users = Users.find({ username: query, @@ -437,7 +302,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { }); } - if (queryParams.assignees.length) { + if (queryParams.assignees && queryParams.assignees.length) { queryParams.assignees.forEach(query => { const users = Users.find({ username: query, @@ -465,7 +330,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { selector.assignees = { $in: queryAssignees }; } - if (queryParams.labels.length) { + if (queryParams.labels && queryParams.labels.length) { queryParams.labels.forEach(label => { const queryLabels = []; @@ -516,16 +381,26 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { }); } - if (queryParams.has.length) { + if (queryParams.has && queryParams.has.length) { queryParams.has.forEach(has => { switch (has.field) { case 'attachment': - const attachments = Attachments.find({}, { fields: { cardId: 1 } }); - selector.$and.push({ _id: { $in: attachments.map(a => a.cardId) } }); + selector.$and.push({ + _id: { + $in: Attachments.find({}, { fields: { cardId: 1 } }).map( + a => a.cardId, + ), + }, + }); break; case 'checklist': - const checklists = Checklists.find({}, { fields: { cardId: 1 } }); - selector.$and.push({ _id: { $in: checklists.map(a => a.cardId) } }); + selector.$and.push({ + _id: { + $in: Checklists.find({}, { fields: { cardId: 1 } }).map( + a => a.cardId, + ), + }, + }); break; case 'description': case 'startAt': @@ -677,7 +552,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { // console.log('projection:', projection); return findCards(sessionId, selector, projection, errors); -}); +} Meteor.publish('brokenCards', function() { const user = Users.findOne({ _id: this.userId }); @@ -773,7 +648,8 @@ function findCards(sessionId, selector, projection, errors = null) { const userId = Meteor.userId(); // eslint-disable-next-line no-console - // console.log('selector:', selector); + console.log('selector:', selector); + console.log('selector.$and:', selector.$and); // eslint-disable-next-line no-console // console.log('projection:', projection); let cards; @@ -879,5 +755,5 @@ function findCards(sessionId, selector, projection, errors = null) { ]; } - return [SessionData.find({ userId: userId, sessionId })]; + return [SessionData.find({ userId, sessionId })]; } From e0b544fc7e0c04bad3348408616c02d3a0028454 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Mon, 1 Mar 2021 20:18:44 +0200 Subject: [PATCH 02/17] Add new Blaze search component --- client/components/main/myCards.js | 2 +- client/lib/cardSearch.js | 174 ++++++++++++++++++++++++++++++ models/usersessiondata.js | 2 +- server/publications/cards.js | 1 - 4 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 client/lib/cardSearch.js diff --git a/client/components/main/myCards.js b/client/components/main/myCards.js index 54aa8f505..03af8b561 100644 --- a/client/components/main/myCards.js +++ b/client/components/main/myCards.js @@ -1,4 +1,4 @@ -import {CardSearchPagedComponent} from "../../lib/cardSearch"; +import { CardSearchPagedComponent } from '../../lib/cardSearch'; const subManager = new SubsManager(); diff --git a/client/lib/cardSearch.js b/client/lib/cardSearch.js new file mode 100644 index 000000000..aea5b1188 --- /dev/null +++ b/client/lib/cardSearch.js @@ -0,0 +1,174 @@ +export class CardSearchPagedComponent extends BlazeComponent { + onCreated() { + this.searching = new ReactiveVar(false); + this.hasResults = new ReactiveVar(false); + this.hasQueryErrors = new ReactiveVar(false); + this.query = new ReactiveVar(''); + this.resultsHeading = new ReactiveVar(''); + this.searchLink = new ReactiveVar(null); + this.results = new ReactiveVar([]); + this.hasNextPage = new ReactiveVar(false); + this.hasPreviousPage = new ReactiveVar(false); + this.resultsCount = 0; + this.totalHits = 0; + this.queryErrors = null; + this.resultsPerPage = 25; + } + + resetSearch() { + this.searching.set(false); + this.results.set([]); + this.hasResults.set(false); + this.hasQueryErrors.set(false); + this.resultsHeading.set(''); + this.resultsCount = 0; + this.totalHits = 0; + this.queryErrors = null; + } + + getSessionData() { + return SessionData.findOne({ + userId: Meteor.userId(), + sessionId: SessionData.getSessionId(), + }); + } + + getResults() { + // eslint-disable-next-line no-console + // console.log('getting results'); + const sessionData = this.getSessionData(); + // eslint-disable-next-line no-console + // console.log('selector:', sessionData.getSelector()); + console.log('session data:', sessionData); + const projection = sessionData.getProjection(); + projection.skip = 0; + const cards = Cards.find({ _id: { $in: sessionData.cards } }, projection); + this.queryErrors = sessionData.errors; + if (this.queryErrors.length) { + this.hasQueryErrors.set(true); + return null; + } + + if (cards) { + this.totalHits = sessionData.totalHits; + this.resultsCount = cards.count(); + this.resultsStart = sessionData.lastHit - this.resultsCount + 1; + this.resultsEnd = sessionData.lastHit; + this.resultsHeading.set(this.getResultsHeading()); + this.results.set(cards); + this.hasNextPage.set(sessionData.lastHit < sessionData.totalHits); + this.hasPreviousPage.set( + sessionData.lastHit - sessionData.resultsCount > 0, + ); + return cards; + } + + this.resultsCount = 0; + return null; + } + + autorunGlobalSearch(params) { + this.searching.set(true); + + this.autorun(() => { + const handle = Meteor.subscribe( + 'globalSearch', + SessionData.getSessionId(), + params, + ); + Tracker.nonreactive(() => { + Tracker.autorun(() => { + if (handle.ready()) { + this.getResults(); + this.searching.set(false); + this.hasResults.set(true); + } + }); + }); + }); + } + + queryErrorMessages() { + const messages = []; + + this.queryErrors.forEach(err => { + let value = err.color ? TAPi18n.__(`color-${err.value}`) : err.value; + if (!value) { + value = err.value; + } + messages.push(TAPi18n.__(err.tag, value)); + }); + + return messages; + } + + nextPage() { + const sessionData = this.getSessionData(); + + this.autorun(() => { + const handle = Meteor.subscribe('nextPage', sessionData.sessionId); + Tracker.nonreactive(() => { + Tracker.autorun(() => { + if (handle.ready()) { + this.getResults(); + this.searching.set(false); + this.hasResults.set(true); + } + }); + }); + }); + } + + previousPage() { + const sessionData = this.getSessionData(); + + this.autorun(() => { + const handle = Meteor.subscribe('previousPage', sessionData.sessionId); + Tracker.nonreactive(() => { + Tracker.autorun(() => { + if (handle.ready()) { + this.getResults(); + this.searching.set(false); + this.hasResults.set(true); + } + }); + }); + }); + } + + getResultsHeading() { + if (this.resultsCount === 0) { + return TAPi18n.__('no-cards-found'); + } else if (this.resultsCount === 1) { + return TAPi18n.__('one-card-found'); + } else if (this.resultsCount === this.totalHits) { + return TAPi18n.__('n-cards-found', this.resultsCount); + } + + return TAPi18n.__('n-n-of-n-cards-found', { + start: this.resultsStart, + end: this.resultsEnd, + total: this.totalHits, + }); + } + + getSearchHref() { + const baseUrl = window.location.href.replace(/([?#].*$|\s*$)/, ''); + return `${baseUrl}?q=${encodeURIComponent(this.query.get())}`; + } + + events() { + return [ + { + 'click .js-next-page'(evt) { + evt.preventDefault(); + this.nextPage(); + }, + 'click .js-previous-page'(evt) { + evt.preventDefault(); + this.previousPage(); + }, + }, + ]; + } +} diff --git a/models/usersessiondata.js b/models/usersessiondata.js index 97f507402..7d7189ca1 100644 --- a/models/usersessiondata.js +++ b/models/usersessiondata.js @@ -184,7 +184,7 @@ function pickleValue(value) { if (value === null) { return null; } else if (typeof value === 'object') { - switch(value.constructor.name) { + switch (value.constructor.name) { case 'RegExp': return { $$class: 'RegExp', diff --git a/server/publications/cards.js b/server/publications/cards.js index a7b3f699c..af1778486 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -6,7 +6,6 @@ Meteor.publish('card', cardId => { }); Meteor.publish('myCards', function(sessionId) { - const queryParams = { users: [Meteor.user().username], // limit: 25, From 28aebaa06b2f1181362a6339026bb42ec4c76193 Mon Sep 17 00:00:00 2001 From: John Supplee Date: Thu, 4 Mar 2021 02:13:22 +0200 Subject: [PATCH 03/17] Make search instructions code more uniform --- client/components/main/globalSearch.js | 85 +++++++++++--------------- i18n/en.i18n.json | 2 +- server/publications/cards.js | 27 +++++--- 3 files changed, 54 insertions(+), 60 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 217ff6daa..0636aee4e 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -477,57 +477,42 @@ class GlobalSearchComponent extends CardSearchPagedComponent { predicate_member: TAPi18n.__('predicate-member'), }; - let text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; - text += `\n${TAPi18n.__('globalSearch-instructions-description', tags)}`; - text += `\n\n${TAPi18n.__('globalSearch-instructions-operators', tags)}`; - + let text = ''; [ - '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')}`; - [ - 'globalSearch-instructions-notes-1', - 'globalSearch-instructions-notes-2', - 'globalSearch-instructions-notes-3', - 'globalSearch-instructions-notes-3-2', - 'globalSearch-instructions-notes-4', - 'globalSearch-instructions-notes-5', - ].forEach(instruction => { - text += `\n* ${TAPi18n.__(instruction, tags)}`; + ['# ', 'globalSearch-instructions-heading'], + ['\n', 'globalSearch-instructions-description'], + ['\n\n', 'globalSearch-instructions-operators'], + ['\n* ', 'globalSearch-instructions-operator-board'], + ['\n* ', 'globalSearch-instructions-operator-list'], + ['\n* ', 'globalSearch-instructions-operator-swimlane'], + ['\n* ', 'globalSearch-instructions-operator-comment'], + ['\n* ', 'globalSearch-instructions-operator-label'], + ['\n* ', 'globalSearch-instructions-operator-hash'], + ['\n* ', 'globalSearch-instructions-operator-user'], + ['\n* ', 'globalSearch-instructions-operator-at'], + ['\n* ', 'globalSearch-instructions-operator-member'], + ['\n* ', 'globalSearch-instructions-operator-assignee'], + ['\n* ', 'globalSearch-instructions-operator-due'], + ['\n* ', 'globalSearch-instructions-operator-created'], + ['\n* ', 'globalSearch-instructions-operator-modified'], + ['\n* ', 'globalSearch-instructions-operator-status'], + ['\n * ', 'globalSearch-instructions-status-archived'], + ['\n * ', 'globalSearch-instructions-status-public'], + ['\n * ', 'globalSearch-instructions-status-private'], + ['\n * ', 'globalSearch-instructions-status-all'], + ['\n * ', 'globalSearch-instructions-status-ended'], + ['\n* ', 'globalSearch-instructions-operator-has'], + ['\n* ', 'globalSearch-instructions-operator-sort'], + ['\n* ', 'globalSearch-instructions-operator-limit'], + ['\n## ', 'heading-notes'], + ['\n* ', 'globalSearch-instructions-notes-1'], + ['\n* ', 'globalSearch-instructions-notes-2'], + ['\n* ', 'globalSearch-instructions-notes-3'], + ['\n* ', 'globalSearch-instructions-notes-3-2'], + ['\n* ', 'globalSearch-instructions-notes-4'], + ['\n* ', 'globalSearch-instructions-notes-5'], + ].forEach(([prefix, instruction]) => { + text += `${prefix}${TAPi18n.__(instruction, tags)}`; }); return text; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 0d435439e..9827dc5bc 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -946,7 +946,7 @@ "globalSearch-instructions-operator-swimlane": "`__operator_swimlane__:` - 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-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*", diff --git a/server/publications/cards.js b/server/publications/cards.js index af1778486..f4abcdfb1 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -94,11 +94,19 @@ function buildQuery(sessionId, queryParams) { }); }); this.notFound.labels.forEach(label => { - messages.push({ - tag: 'label-not-found', - value: label, - color: Boards.labelColors().includes(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.users.forEach(user => { messages.push({ tag: 'user-username-not-found', value: user }); @@ -643,7 +651,7 @@ Meteor.publish('previousPage', function(sessionId) { return findCards(sessionId, session.getSelector(), projection); }); -function findCards(sessionId, selector, projection, errors = null) { +function findCards(sessionId, selector, projection, errors = []) { const userId = Meteor.userId(); // eslint-disable-next-line no-console @@ -666,11 +674,12 @@ function findCards(sessionId, selector, projection, errors = null) { cards: [], selector: SessionData.pickle(selector), projection: SessionData.pickle(projection), + errors: errors.errorMessages(), }, }; - if (errors) { - update.$set.errors = errors.errorMessages(); - } + // if (errors) { + // update.$set.errors = errors.errorMessages(); + // } if (cards) { update.$set.totalHits = cards.count(); From 58020863a898acca69cdc5341c568b3bd528803a Mon Sep 17 00:00:00 2001 From: John Supplee <john@supplee.com> Date: Fri, 5 Mar 2021 21:20:55 +0200 Subject: [PATCH 04/17] Handle subscriptions better * use onReady and onStop callbacks when subscribing * show an server error message when the server returns an error * call stop() on subscriptions --- client/components/main/dueCards.js | 2 +- client/components/main/globalSearch.jade | 6 ++ client/components/main/globalSearch.js | 2 +- client/components/main/myCards.js | 2 +- client/lib/cardSearch.js | 103 ++++++++++++----------- i18n/en.i18n.json | 2 + 6 files changed, 65 insertions(+), 52 deletions(-) diff --git a/client/components/main/dueCards.js b/client/components/main/dueCards.js index ac3ab3164..55043d2f2 100644 --- a/client/components/main/dueCards.js +++ b/client/components/main/dueCards.js @@ -57,7 +57,7 @@ class DueCardsComponent extends CardSearchPagedComponent { queryParams.users = [Meteor.user().username]; } - this.autorunGlobalSearch(queryParams); + this.runGlobalSearch(queryParams); } dueCardsView() { diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 457461965..5389fb780 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -50,6 +50,12 @@ template(name="globalSearch") = msg else +resultsPaged(this) + else if serverError.get + .global-search-page + .global-search-help + h1 {{_ 'server-error' }} + +viewer + | {{_ 'server-error-troubleshooting' }} else .global-search-page .global-search-help diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 0636aee4e..0290fb7af 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -431,7 +431,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { return; } - this.autorunGlobalSearch(params); + this.runGlobalSearch(params); } searchInstructions() { diff --git a/client/components/main/myCards.js b/client/components/main/myCards.js index 03af8b561..13e5ec93a 100644 --- a/client/components/main/myCards.js +++ b/client/components/main/myCards.js @@ -53,7 +53,7 @@ class MyCardsComponent extends CardSearchPagedComponent { sort: { name: 'dueAt', order: 'des' }, }; - this.autorunGlobalSearch(queryParams); + this.runGlobalSearch(queryParams); Meteor.subscribe('setting'); } diff --git a/client/lib/cardSearch.js b/client/lib/cardSearch.js index aea5b1188..aead813e2 100644 --- a/client/lib/cardSearch.js +++ b/client/lib/cardSearch.js @@ -13,6 +13,28 @@ export class CardSearchPagedComponent extends BlazeComponent { this.totalHits = 0; this.queryErrors = null; this.resultsPerPage = 25; + this.sessionId = SessionData.getSessionId(); + this.subscriptionHandle = null; + this.serverError = new ReactiveVar(false); + + const that = this; + this.subscriptionCallbacks = { + onReady() { + that.getResults(); + that.searching.set(false); + that.hasResults.set(true); + that.serverError.set(false); + }, + onError(error) { + that.searching.set(false); + that.hasResults.set(false); + that.serverError.set(true); + console.log('Error.reason:', error.reason); + console.log('Error.message:', error.message); + console.log('Error.stack:', error.stack); + } + }; + } resetSearch() { @@ -21,15 +43,15 @@ export class CardSearchPagedComponent extends BlazeComponent { this.hasResults.set(false); this.hasQueryErrors.set(false); this.resultsHeading.set(''); + this.serverError.set(false); this.resultsCount = 0; this.totalHits = 0; this.queryErrors = null; } - getSessionData() { + getSessionData(sessionId) { return SessionData.findOne({ - userId: Meteor.userId(), - sessionId: SessionData.getSessionId(), + sessionId: sessionId ? sessionId : SessionData.getSessionId(), }); } @@ -45,6 +67,7 @@ export class CardSearchPagedComponent extends BlazeComponent { const cards = Cards.find({ _id: { $in: sessionData.cards } }, projection); this.queryErrors = sessionData.errors; if (this.queryErrors.length) { + // console.log('queryErrors:', this.queryErrorMessages()); this.hasQueryErrors.set(true); return null; } @@ -67,25 +90,21 @@ export class CardSearchPagedComponent extends BlazeComponent { return null; } - autorunGlobalSearch(params) { - this.searching.set(true); + stopSubscription() { + if (this.subscriptionHandle) { + this.subscriptionHandle.stop(); + } + } - this.autorun(() => { - const handle = Meteor.subscribe( - 'globalSearch', - SessionData.getSessionId(), - params, - ); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - if (handle.ready()) { - this.getResults(); - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); + runGlobalSearch(params) { + this.searching.set(true); + this.stopSubscription(); + this.subscriptionHandle = Meteor.subscribe( + 'globalSearch', + this.sessionId, + params, + this.subscriptionCallbacks, + ); } queryErrorMessages() { @@ -103,37 +122,23 @@ export class CardSearchPagedComponent extends BlazeComponent { } nextPage() { - const sessionData = this.getSessionData(); - - this.autorun(() => { - const handle = Meteor.subscribe('nextPage', sessionData.sessionId); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - if (handle.ready()) { - this.getResults(); - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); + this.searching.set(true); + this.stopSubscription(); + this.subscriptionHandle = Meteor.subscribe( + 'nextPage', + this.sessionId, + this.subscriptionCallbacks, + ); } previousPage() { - const sessionData = this.getSessionData(); - - this.autorun(() => { - const handle = Meteor.subscribe('previousPage', sessionData.sessionId); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - if (handle.ready()) { - this.getResults(); - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); + this.searching.set(true); + this.stopSubscription(); + this.subscriptionHandle = Meteor.subscribe( + 'previousPage', + this.sessionId, + this.subscriptionCallbacks, + ); } getResultsHeading() { diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 9827dc5bc..932bb5dd9 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -978,6 +978,8 @@ "sort-cards": "Sort Cards", "cardsSortPopup-title": "Sort Cards", "due-date": "Due Date", + "server-error": "Server Error", + "server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation on Linux, run: `sudo journalctl -u 'snap.wekan.*'`", "title-alphabetically": "Title (Alphabetically)", "created-at-newest-first": "Created At (Newest First)", "created-at-oldest-first": "Created At (Oldest First)" From 8f978c23b4e0d920688c5319241dc5f6c29333f4 Mon Sep 17 00:00:00 2001 From: John Supplee <john@supplee.com> Date: Fri, 5 Mar 2021 23:47:12 +0200 Subject: [PATCH 05/17] Modify Broken Cards to use new search functions and component --- client/components/cards/resultCard.jade | 24 +- client/components/main/brokenCards.jade | 48 +--- client/components/main/brokenCards.js | 26 +- server/publications/cards.js | 306 +++++++++++------------- 4 files changed, 184 insertions(+), 220 deletions(-) diff --git a/client/components/cards/resultCard.jade b/client/components/cards/resultCard.jade index 6cff985aa..cf001532b 100644 --- a/client/components/cards/resultCard.jade +++ b/client/components/cards/resultCard.jade @@ -6,8 +6,12 @@ template(name="resultCard") ul.result-card-context-list li.result-card-context(title="{{_ 'board'}}") .result-card-block-wrapper - +viewer - = getBoard.title + if boardId + +viewer + = getBoard.title + else + .broken-cards-null + | NULL if getBoard.archived i.fa.fa-archive li.result-card-context.result-card-context-separator @@ -16,8 +20,12 @@ template(name="resultCard") = ' ' li.result-card-context(title="{{_ 'swimlane'}}") .result-card-block-wrapper - +viewer - = getSwimlane.title + if swimlaneId + +viewer + = getSwimlane.title + else + .broken-cards-null + | NULL if getSwimlane.archived i.fa.fa-archive li.result-card-context.result-card-context-separator @@ -26,7 +34,11 @@ template(name="resultCard") = ' ' li.result-card-context(title="{{_ 'list'}}") .result-card-block-wrapper - +viewer - = getList.title + if listId + +viewer + = getList.title + else + .broken-cards-null + | NULL if getList.archived i.fa.fa-archive diff --git a/client/components/main/brokenCards.jade b/client/components/main/brokenCards.jade index 6986f31dd..9d5828905 100644 --- a/client/components/main/brokenCards.jade +++ b/client/components/main/brokenCards.jade @@ -3,39 +3,15 @@ template(name="brokenCardsHeaderBar") | {{_ 'broken-cards'}} template(name="brokenCards") - .wrapper - .broken-cards-wrapper - each card in brokenCardsList - .broken-cards-card-wrapper - .broken-cards-card-title - = card.title - ul.broken-cards-context-list - li.broken-cards-context(title="{{_ 'board'}}") - if card.boardId - +viewer - = card.getBoard.title - else - .broken-cards-null - | NULL - li.broken-cards-context.broken-cards-context-separator - = ' ' - | {{_ 'context-separator'}} - = ' ' - li.broken-cards-context(title="{{_ 'swimlane'}}") - if card.swimlaneId - +viewer - = card.getSwimlane.title - else - .broken-cards-null - | NULL - li.broken-cards-context - = ' ' - | {{_ 'context-separator'}} - = ' ' - li.broken-cards-context(title="{{_ 'list'}}") - if card.listId - +viewer - = card.getList.title - else - .broken-cards-null - | NULL + if currentUser + if searching.get + +spinner + else if hasResults.get + .global-search-results-list-wrapper + if hasQueryErrors.get + div + each msg in errorMessages + span.global-search-error-messages + = msg + else + +resultsPaged(this) diff --git a/client/components/main/brokenCards.js b/client/components/main/brokenCards.js index 6348a50ed..4338d5c02 100644 --- a/client/components/main/brokenCards.js +++ b/client/components/main/brokenCards.js @@ -1,3 +1,5 @@ +import { CardSearchPagedComponent } from "../../lib/cardSearch"; + BlazeComponent.extendComponent({}).register('brokenCardsHeaderBar'); Template.brokenCards.helpers({ @@ -6,23 +8,11 @@ Template.brokenCards.helpers({ }, }); -BlazeComponent.extendComponent({ +class BrokenCardsComponent extends CardSearchPagedComponent { onCreated() { - Meteor.subscribe('setting'); - Meteor.subscribe('brokenCards'); - }, + super.onCreated(); - brokenCardsList() { - const selector = { - $or: [ - { boardId: { $in: [null, ''] } }, - { swimlaneId: { $in: [null, ''] } }, - { listId: { $in: [null, ''] } }, - { permission: 'public' }, - { members: { $elemMatch: { userId: user._id, isActive: true } } }, - ], - }; - - return Cards.find(selector); - }, -}).register('brokenCards'); + Meteor.subscribe('brokenCards', this.sessionId); + } +} +BrokenCardsComponent.register('brokenCards'); diff --git a/server/publications/cards.js b/server/publications/cards.js index 7103db575..45ad94f7d 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -13,7 +13,7 @@ Meteor.publish('myCards', function(sessionId) { // sort: { name: 'dueAt', order: 'des' }, }; - return buildQuery(sessionId, queryParams); + return findCards(sessionId, buildQuery(queryParams)); }); // Meteor.publish('dueCards', function(sessionId, allUsers = false) { @@ -44,93 +44,104 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { // eslint-disable-next-line no-console // console.log('queryParams:', queryParams); - return buildQuery(sessionId, queryParams); + return findCards(sessionId, buildQuery(queryParams)); }); -function buildQuery(sessionId, queryParams) { +class QueryErrors { + constructor() { + this.notFound = { + boards: [], + swimlanes: [], + lists: [], + labels: [], + users: [], + members: [], + assignees: [], + status: [], + comments: [], + }; + + this.colorMap = Boards.colorMap(); + } + + hasErrors() { + for (const value of Object.values(this.notFound)) { + if (value.length) { + return true; + } + } + return false; + } + + errorMessages() { + const messages = []; + + this.notFound.boards.forEach(board => { + messages.push({ tag: 'board-title-not-found', value: board }); + }); + this.notFound.swimlanes.forEach(swim => { + messages.push({ tag: 'swimlane-title-not-found', value: swim }); + }); + this.notFound.lists.forEach(list => { + messages.push({ tag: 'list-title-not-found', value: list }); + }); + this.notFound.comments.forEach(comments => { + comments.forEach(text => { + messages.push({ tag: 'comment-not-found', value: text }); + }); + }); + this.notFound.labels.forEach(label => { + 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.users.forEach(user => { + messages.push({ tag: 'user-username-not-found', value: user }); + }); + this.notFound.members.forEach(user => { + messages.push({ tag: 'user-username-not-found', value: user }); + }); + this.notFound.assignees.forEach(user => { + messages.push({ tag: 'user-username-not-found', value: user }); + }); + + return messages; + } +}; + +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(); - const errors = new (class { - constructor() { - this.notFound = { - boards: [], - swimlanes: [], - lists: [], - labels: [], - users: [], - members: [], - assignees: [], - status: [], - comments: [], - }; - - this.colorMap = Boards.colorMap(); - } - - hasErrors() { - for (const value of Object.values(this.notFound)) { - if (value.length) { - return true; - } - } - return false; - } - - errorMessages() { - const messages = []; - - this.notFound.boards.forEach(board => { - messages.push({ tag: 'board-title-not-found', value: board }); - }); - this.notFound.swimlanes.forEach(swim => { - messages.push({ tag: 'swimlane-title-not-found', value: swim }); - }); - this.notFound.lists.forEach(list => { - messages.push({ tag: 'list-title-not-found', value: list }); - }); - this.notFound.comments.forEach(comments => { - comments.forEach(text => { - messages.push({ tag: 'comment-not-found', value: text }); - }); - }); - this.notFound.labels.forEach(label => { - 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.users.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - this.notFound.members.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - this.notFound.assignees.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - - return messages; - } - })(); + errors = new QueryErrors(); let selector = {}; - let skip = 0; - if (queryParams.skip) { - skip = queryParams.skip; - } - let limit = 25; - if (queryParams.limit) { - limit = queryParams.limit; - } if (queryParams.selector) { selector = queryParams.selector; @@ -487,6 +498,25 @@ function buildQuery(sessionId, queryParams) { // eslint-disable-next-line no-console // console.log('selector.$and:', selector.$and); + const query = new Query(); + query.selector = selector; + query.params = queryParams; + query.errors = errors; + + return query; +} + +function buildProjection(query) { + + let skip = 0; + if (query.params.skip) { + skip = query.params.skip; + } + let limit = 25; + if (query.params.limit) { + limit = query.params.limit; + } + const projection = { fields: { _id: 1, @@ -516,9 +546,9 @@ function buildQuery(sessionId, queryParams) { limit, }; - if (queryParams.sort) { - const order = queryParams.sort.order === 'asc' ? 1 : -1; - switch (queryParams.sort.name) { + if (query.params.sort) { + const order = query.params.sort.order === 'asc' ? 1 : -1; + switch (query.params.sort.name) { case 'dueAt': projection.sort = { dueAt: order, @@ -561,77 +591,33 @@ function buildQuery(sessionId, queryParams) { // eslint-disable-next-line no-console // console.log('projection:', projection); - return findCards(sessionId, selector, projection, errors); + query.projection = projection; + + return query; } -Meteor.publish('brokenCards', function() { - const user = Users.findOne({ _id: this.userId }); +function buildQuery(queryParams) { + const query = buildSelector(queryParams); - const permiitedBoards = [null]; - let selector = {}; - selector.$or = [ - { permission: 'public' }, - { members: { $elemMatch: { userId: user._id, isActive: true } } }, - ]; + return buildProjection(query); +} - Boards.find(selector).forEach(board => { - permiitedBoards.push(board._id); - }); +Meteor.publish('brokenCards', function(sessionId) { - selector = { - boardId: { $in: permiitedBoards }, - $or: [ - { boardId: { $in: [null, ''] } }, - { swimlaneId: { $in: [null, ''] } }, - { listId: { $in: [null, ''] } }, - ], + const queryParams = { + users: [Meteor.user().username], + // limit: 25, + skip: 0, + // sort: { name: 'dueAt', order: 'des' }, }; - - 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 }), + const query = buildQuery(queryParams); + query.selector.$or = [ + { boardId: { $in: [null, ''] } }, + { swimlaneId: { $in: [null, ''] } }, + { listId: { $in: [null, ''] } }, ]; + + return findCards(sessionId, query); }); Meteor.publish('nextPage', function(sessionId) { @@ -641,7 +627,7 @@ Meteor.publish('nextPage', function(sessionId) { const projection = session.getProjection(); projection.skip = session.lastHit; - return findCards(sessionId, session.getSelector(), projection); + return findCards(sessionId, new Query(session.getSelector(), projection)); }); Meteor.publish('previousPage', function(sessionId) { @@ -651,20 +637,20 @@ Meteor.publish('previousPage', function(sessionId) { const projection = session.getProjection(); projection.skip = session.lastHit - session.resultsCount - projection.limit; - return findCards(sessionId, session.getSelector(), projection); + return findCards(sessionId, new Query(session.getSelector(), projection)); }); -function findCards(sessionId, selector, projection, errors = []) { +function findCards(sessionId, query) { const userId = Meteor.userId(); // eslint-disable-next-line no-console - console.log('selector:', selector); - console.log('selector.$and:', selector.$and); + console.log('selector:', query.selector); + console.log('selector.$and:', query.selector.$and); // eslint-disable-next-line no-console // console.log('projection:', projection); let cards; if (!errors || !errors.hasErrors()) { - cards = Cards.find(selector, projection); + cards = Cards.find(query.selector, query.projection); } // eslint-disable-next-line no-console // console.log('count:', cards.count()); @@ -675,9 +661,9 @@ function findCards(sessionId, selector, projection, errors = []) { lastHit: 0, resultsCount: 0, cards: [], - selector: SessionData.pickle(selector), - projection: SessionData.pickle(projection), - errors: errors.errorMessages(), + selector: SessionData.pickle(query.selector), + projection: SessionData.pickle(query.projection), + errors: query.errors.errorMessages(), }, }; // if (errors) { @@ -687,8 +673,8 @@ function findCards(sessionId, selector, projection, errors = []) { if (cards) { update.$set.totalHits = cards.count(); update.$set.lastHit = - projection.skip + projection.limit < cards.count() - ? projection.skip + projection.limit + query.projection.skip + query.projection.limit < cards.count() + ? query.projection.skip + query.projection.limit : cards.count(); update.$set.cards = cards.map(card => { return card._id; From 906118098348bdb9c2da389faf8417f10df58e95 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" <john@supplee.com> Date: Sun, 7 Mar 2021 02:12:31 +0200 Subject: [PATCH 06/17] Code cleanup --- client/components/main/brokenCards.js | 2 +- client/components/main/dueCards.js | 6 +- client/components/main/globalSearch.js | 1 + client/components/main/myCards.js | 6 +- client/lib/cardSearch.js | 3 +- server/publications/cards.js | 86 +++++++++++++------------- 6 files changed, 51 insertions(+), 53 deletions(-) diff --git a/client/components/main/brokenCards.js b/client/components/main/brokenCards.js index 4338d5c02..17d30f37c 100644 --- a/client/components/main/brokenCards.js +++ b/client/components/main/brokenCards.js @@ -1,4 +1,4 @@ -import { CardSearchPagedComponent } from "../../lib/cardSearch"; +import { CardSearchPagedComponent } from '../../lib/cardSearch'; BlazeComponent.extendComponent({}).register('brokenCardsHeaderBar'); diff --git a/client/components/main/dueCards.js b/client/components/main/dueCards.js index 55043d2f2..7c329586f 100644 --- a/client/components/main/dueCards.js +++ b/client/components/main/dueCards.js @@ -1,6 +1,6 @@ import { CardSearchPagedComponent } from '../../lib/cardSearch'; -const subManager = new SubsManager(); +// const subManager = new SubsManager(); BlazeComponent.extendComponent({ dueCardsView() { @@ -81,8 +81,8 @@ class DueCardsComponent extends CardSearchPagedComponent { } cards.sort((a, b) => { - const x = a.dueAt === null ? Date('2100-12-31') : a.dueAt; - const y = b.dueAt === null ? Date('2100-12-31') : b.dueAt; + const x = a.dueAt === null ? new Date('2100-12-31') : a.dueAt; + const y = b.dueAt === null ? new Date('2100-12-31') : b.dueAt; if (x > y) return 1; else if (x < y) return -1; diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 0290fb7af..d04eedaa7 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -1,4 +1,5 @@ import { CardSearchPagedComponent } from '../../lib/cardSearch'; +import moment from 'moment'; // const subManager = new SubsManager(); diff --git a/client/components/main/myCards.js b/client/components/main/myCards.js index 13e5ec93a..9307b9d32 100644 --- a/client/components/main/myCards.js +++ b/client/components/main/myCards.js @@ -1,6 +1,6 @@ import { CardSearchPagedComponent } from '../../lib/cardSearch'; -const subManager = new SubsManager(); +// const subManager = new SubsManager(); BlazeComponent.extendComponent({ myCardsSort() { @@ -180,8 +180,8 @@ class MyCardsComponent extends CardSearchPagedComponent { }); cards.sort((a, b) => { - const x = a.dueAt === null ? Date('2100-12-31') : a.dueAt; - const y = b.dueAt === null ? Date('2100-12-31') : b.dueAt; + const x = a.dueAt === null ? new Date('2100-12-31') : a.dueAt; + const y = b.dueAt === null ? new Date('2100-12-31') : b.dueAt; if (x > y) return 1; else if (x < y) return -1; diff --git a/client/lib/cardSearch.js b/client/lib/cardSearch.js index aead813e2..95b5324b9 100644 --- a/client/lib/cardSearch.js +++ b/client/lib/cardSearch.js @@ -32,9 +32,8 @@ export class CardSearchPagedComponent extends BlazeComponent { console.log('Error.reason:', error.reason); console.log('Error.message:', error.message); console.log('Error.stack:', error.stack); - } + }, }; - } resetSearch() { diff --git a/server/publications/cards.js b/server/publications/cards.js index 45ad94f7d..f6421e988 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -1,3 +1,14 @@ +import Users from '../../models/users'; +import Boards from '../../models/boards'; +import Lists from '../../models/lists'; +import Swimlanes from '../../models/swimlanes'; +import CardComments from '../../models/cardComments'; +import Attachments from '../../models/attachments'; +import Checklists from '../../models/checklists'; +import ChecklistItems from '../../models/checklistItems'; +import SessionData from '../../models/usersessiondata'; +import CustomFields from '../../models/customFields'; + const escapeForRegex = require('escape-string-regexp'); Meteor.publish('card', cardId => { @@ -117,7 +128,7 @@ class QueryErrors { return messages; } -}; +} class Query { params = {}; @@ -139,7 +150,7 @@ class Query { function buildSelector(queryParams) { const userId = Meteor.userId(); - errors = new QueryErrors(); + const errors = new QueryErrors(); let selector = {}; @@ -287,8 +298,10 @@ function buildSelector(queryParams) { } }); - const queryMembers = []; - const queryAssignees = []; + const queryUsers = { + members: [], + assignees: [], + }; if (queryParams.users && queryParams.users.length) { queryParams.users.forEach(query => { const users = Users.find({ @@ -296,8 +309,8 @@ function buildSelector(queryParams) { }); if (users.count()) { users.forEach(user => { - queryMembers.push(user._id); - queryAssignees.push(user._id); + queryUsers.members.push(user._id); + queryUsers.assignees.push(user._id); }); } else { errors.notFound.users.push(query); @@ -305,47 +318,34 @@ function buildSelector(queryParams) { }); } - if (queryParams.members && queryParams.members.length) { - queryParams.members.forEach(query => { - const users = Users.find({ - username: query, - }); - if (users.count()) { - users.forEach(user => { - queryMembers.push(user._id); + ['members', 'assignees'].forEach(key => { + if (queryParams[key] && queryParams[key].length) { + queryParams[key].forEach(query => { + const users = Users.find({ + username: query, }); - } else { - errors.notFound.members.push(query); - } - }); - } - - if (queryParams.assignees && queryParams.assignees.length) { - queryParams.assignees.forEach(query => { - const users = Users.find({ - username: query, + if (users.count()) { + users.forEach(user => { + queryUsers[key].push(user._id); + }); + } else { + errors.notFound[key].push(query); + } }); - if (users.count()) { - users.forEach(user => { - queryAssignees.push(user._id); - }); - } else { - errors.notFound.assignees.push(query); - } - }); - } + } + }); - if (queryMembers.length && queryAssignees.length) { + if (queryUsers.members.length && queryUsers.assignees.length) { selector.$and.push({ $or: [ - { members: { $in: queryMembers } }, - { assignees: { $in: queryAssignees } }, + { members: { $in: queryUsers.members } }, + { assignees: { $in: queryUsers.assignees } }, ], }); - } else if (queryMembers.length) { - selector.members = { $in: queryMembers }; - } else if (queryAssignees.length) { - selector.assignees = { $in: queryAssignees }; + } else if (queryUsers.members.length) { + selector.members = { $in: queryUsers.members }; + } else if (queryUsers.assignees.length) { + selector.assignees = { $in: queryUsers.assignees }; } if (queryParams.labels && queryParams.labels.length) { @@ -398,7 +398,7 @@ function buildSelector(queryParams) { } } - selector.labelIds = { $in: queryLabels }; + selector.labelIds = { $in: _.uniq(queryLabels) }; }); } @@ -507,7 +507,6 @@ function buildSelector(queryParams) { } function buildProjection(query) { - let skip = 0; if (query.params.skip) { skip = query.params.skip; @@ -603,7 +602,6 @@ function buildQuery(queryParams) { } Meteor.publish('brokenCards', function(sessionId) { - const queryParams = { users: [Meteor.user().username], // limit: 25, @@ -649,7 +647,7 @@ function findCards(sessionId, query) { // eslint-disable-next-line no-console // console.log('projection:', projection); let cards; - if (!errors || !errors.hasErrors()) { + if (!query.errors || !query.errors.hasErrors()) { cards = Cards.find(query.selector, query.projection); } // eslint-disable-next-line no-console From 4ed5c3e4b82ed1d01801ab8a0b5c6bad531d0dd0 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" <john@supplee.com> Date: Sun, 7 Mar 2021 02:12:58 +0200 Subject: [PATCH 07/17] Create array of cards from card search result card ids instead of re-running the search on the client side. --- client/lib/cardSearch.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/lib/cardSearch.js b/client/lib/cardSearch.js index 95b5324b9..5af6a974f 100644 --- a/client/lib/cardSearch.js +++ b/client/lib/cardSearch.js @@ -61,9 +61,10 @@ export class CardSearchPagedComponent extends BlazeComponent { // eslint-disable-next-line no-console // console.log('selector:', sessionData.getSelector()); console.log('session data:', sessionData); - const projection = sessionData.getProjection(); - projection.skip = 0; - const cards = Cards.find({ _id: { $in: sessionData.cards } }, projection); + const cards = []; + sessionData.cards.forEach(cardId => { + cards.push(Cards.findOne({ _id: cardId })); + }); this.queryErrors = sessionData.errors; if (this.queryErrors.length) { // console.log('queryErrors:', this.queryErrorMessages()); @@ -73,7 +74,7 @@ export class CardSearchPagedComponent extends BlazeComponent { if (cards) { this.totalHits = sessionData.totalHits; - this.resultsCount = cards.count(); + this.resultsCount = cards.length; this.resultsStart = sessionData.lastHit - this.resultsCount + 1; this.resultsEnd = sessionData.lastHit; this.resultsHeading.set(this.getResultsHeading()); From 849b608933bf9cc7156242fc3554441c7d95c784 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" <john@supplee.com> Date: Mon, 8 Mar 2021 19:18:01 +0200 Subject: [PATCH 08/17] Start adding constants for search operators and predicates --- client/components/main/globalSearch.js | 121 +++++++++++++++--------- config/search-const.js | 30 ++++++ server/publications/cards.js | 123 +++++++++++++++---------- 3 files changed, 180 insertions(+), 94 deletions(-) create mode 100644 config/search-const.js diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index d04eedaa7..fbc61c37f 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -1,5 +1,33 @@ import { CardSearchPagedComponent } from '../../lib/cardSearch'; import moment from 'moment'; +import { + OPERATOR_BOARD, + OPERATOR_DUE, OPERATOR_LIST, + OPERATOR_SWIMLANE, OPERATOR_USER, + ORDER_ASCENDING, + ORDER_DESCENDING, + PREDICATE_ALL, + PREDICATE_ARCHIVED, + PREDICATE_ASSIGNEES, + PREDICATE_ATTACHMENT, + PREDICATE_CHECKLIST, + PREDICATE_CREATED_AT, + PREDICATE_DESCRIPTION, + PREDICATE_DUE_AT, + PREDICATE_END_AT, + PREDICATE_ENDED, + PREDICATE_MEMBERS, + PREDICATE_MODIFIED_AT, + PREDICATE_MONTH, + PREDICATE_OPEN, + PREDICATE_OVERDUE, + PREDICATE_PRIVATE, + PREDICATE_PUBLIC, + PREDICATE_QUARTER, + PREDICATE_START_AT, + PREDICATE_WEEK, + PREDICATE_YEAR, +} from '../../../config/search-const'; // const subManager = new SubsManager(); @@ -135,22 +163,22 @@ class GlobalSearchComponent extends CardSearchPagedComponent { const reNegatedOperator = new RegExp('^-(?<operator>.*)$'); const operators = { - 'operator-board': 'boards', - 'operator-board-abbrev': 'boards', - 'operator-swimlane': 'swimlanes', - 'operator-swimlane-abbrev': 'swimlanes', - 'operator-list': 'lists', - 'operator-list-abbrev': 'lists', + 'operator-board': OPERATOR_BOARD, + 'operator-board-abbrev': OPERATOR_BOARD, + 'operator-swimlane': OPERATOR_SWIMLANE, + 'operator-swimlane-abbrev': OPERATOR_SWIMLANE, + 'operator-list': OPERATOR_LIST, + 'operator-list-abbrev': OPERATOR_LIST, 'operator-label': 'labels', 'operator-label-abbrev': 'labels', - 'operator-user': 'users', - 'operator-user-abbrev': 'users', + '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-due': 'dueAt', + 'operator-due': OPERATOR_DUE, 'operator-created': 'createdAt', 'operator-modified': 'modifiedAt', 'operator-comment': 'comments', @@ -161,36 +189,36 @@ class GlobalSearchComponent extends CardSearchPagedComponent { const predicates = { due: { - 'predicate-overdue': 'overdue', + 'predicate-overdue': PREDICATE_OVERDUE, }, durations: { - 'predicate-week': 'week', - 'predicate-month': 'month', - 'predicate-quarter': 'quarter', - 'predicate-year': 'year', + 'predicate-week': PREDICATE_WEEK, + 'predicate-month': PREDICATE_MONTH, + 'predicate-quarter': PREDICATE_QUARTER, + 'predicate-year': PREDICATE_YEAR, }, status: { - 'predicate-archived': 'archived', - 'predicate-all': 'all', - 'predicate-open': 'open', - 'predicate-ended': 'ended', - 'predicate-public': 'public', - 'predicate-private': 'private', + 'predicate-archived': PREDICATE_ARCHIVED, + 'predicate-all': PREDICATE_ALL, + 'predicate-open': PREDICATE_OPEN, + 'predicate-ended': PREDICATE_ENDED, + 'predicate-public': PREDICATE_PUBLIC, + 'predicate-private': PREDICATE_PRIVATE, }, sorts: { - 'predicate-due': 'dueAt', - 'predicate-created': 'createdAt', - 'predicate-modified': 'modifiedAt', + 'predicate-due': PREDICATE_DUE_AT, + 'predicate-created': PREDICATE_CREATED_AT, + 'predicate-modified': PREDICATE_MODIFIED_AT, }, has: { - 'predicate-description': 'description', - 'predicate-checklist': 'checklist', - 'predicate-attachment': 'attachment', - 'predicate-start': 'startAt', - 'predicate-end': 'endAt', - 'predicate-due': 'dueAt', - 'predicate-assignee': 'assignees', - 'predicate-member': 'members', + 'predicate-description': PREDICATE_DESCRIPTION, + 'predicate-checklist': PREDICATE_CHECKLIST, + 'predicate-attachment': PREDICATE_ATTACHMENT, + 'predicate-start': PREDICATE_START_AT, + 'predicate-end': PREDICATE_END_AT, + 'predicate-due': PREDICATE_DUE_AT, + 'predicate-assignee': PREDICATE_ASSIGNEES, + 'predicate-member': PREDICATE_MEMBERS, }, }; const predicateTranslations = {}; @@ -212,20 +240,25 @@ class GlobalSearchComponent extends CardSearchPagedComponent { const params = { limit: this.resultsPerPage, - boards: [], - swimlanes: [], - lists: [], - users: [], + // boards: [], + // swimlanes: [], + // lists: [], + // users: [], members: [], assignees: [], labels: [], status: [], - dueAt: null, + // dueAt: null, createdAt: null, modifiedAt: null, comments: [], has: [], }; + params[OPERATOR_BOARD] = []; + params[OPERATOR_DUE] = null; + params[OPERATOR_LIST] = []; + params[OPERATOR_SWIMLANE] = []; + params[OPERATOR_USER] = []; let text = ''; while (query) { @@ -254,7 +287,9 @@ class GlobalSearchComponent extends CardSearchPagedComponent { value = this.colorMap[value]; // console.log('found color:', value); } - } else if (['dueAt', 'createdAt', 'modifiedAt'].includes(operator)) { + } else if ( + [OPERATOR_DUE, 'createdAt', 'modifiedAt'].includes(operator) + ) { const days = parseInt(value, 10); let duration = null; if (isNaN(days)) { @@ -263,7 +298,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { duration = predicateTranslations.durations[value]; let date = null; switch (duration) { - case 'week': + case PREDICATE_WEEK: // eslint-disable-next-line no-case-declarations const week = moment().week(); if (week === 52) { @@ -273,7 +308,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { date = moment(week + 1, 'W'); } break; - case 'month': + case PREDICATE_MONTH: // eslint-disable-next-line no-case-declarations const month = moment().month(); // .month() is zero indexed @@ -284,7 +319,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { date = moment(month + 2, 'M'); } break; - case 'quarter': + case PREDICATE_QUARTER: // eslint-disable-next-line no-case-declarations const quarter = moment().quarter(); if (quarter === 4) { @@ -294,7 +329,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { date = moment(quarter + 1, 'Q'); } break; - case 'year': + case PREDICATE_YEAR: date = moment(moment().year() + 1, 'YYYY'); break; } @@ -304,7 +339,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { value: date.format('YYYY-MM-DD'), }; } - } else if (operator === 'dueAt' && value === 'overdue') { + } else if (operator === 'dueAt' && value === PREDICATE_OVERDUE) { value = { operator: '$lt', value: moment().format('YYYY-MM-DD'), @@ -346,7 +381,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { } else { value = { name: predicateTranslations.sorts[value], - order: negated ? 'des' : 'asc', + order: negated ? ORDER_DESCENDING : ORDER_ASCENDING, }; } } else if (operator === 'status') { diff --git a/config/search-const.js b/config/search-const.js new file mode 100644 index 000000000..3bebf3490 --- /dev/null +++ b/config/search-const.js @@ -0,0 +1,30 @@ +export const OPERATOR_DUE = 'dueAt'; +export const OPERATOR_BOARD = 'board'; +export const OPERATOR_LABEL = 'label'; +export const OPERATOR_LIST = 'list'; +export const OPERATOR_SWIMLANE = 'swimlane'; +export const OPERATOR_USER = 'user'; +export const ORDER_ASCENDING = 'asc'; +export const ORDER_DESCENDING = 'des'; +export const PREDICATE_ALL = 'all'; +export const PREDICATE_ARCHIVED = 'archived'; +export const PREDICATE_ASSIGNEES = 'assignees'; +export const PREDICATE_ATTACHMENT = 'attachment'; +export const PREDICATE_CHECKLIST = 'checklist'; +export const PREDICATE_CREATED_AT = 'createdAt'; +export const PREDICATE_DESCRIPTION = 'description'; +export const PREDICATE_DUE_AT = 'dueAt'; +export const PREDICATE_END_AT = 'endAt'; +export const PREDICATE_ENDED = 'ended'; +export const PREDICATE_MEMBERS = 'members'; +export const PREDICATE_MODIFIED_AT = 'modifiedAt'; +export const PREDICATE_MONTH = 'month'; +export const PREDICATE_OPEN = 'open'; +export const PREDICATE_OVERDUE = 'overdue'; +export const PREDICATE_PRIVATE = 'private'; +export const PREDICATE_PUBLIC = 'public'; +export const PREDICATE_QUARTER = 'quarter'; +export const PREDICATE_START_AT = 'startAt'; +export const PREDICATE_SYSTEM = 'system'; +export const PREDICATE_WEEK = 'week'; +export const PREDICATE_YEAR = 'year'; diff --git a/server/publications/cards.js b/server/publications/cards.js index f6421e988..3c79a0405 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -8,6 +8,27 @@ import Checklists from '../../models/checklists'; import ChecklistItems from '../../models/checklistItems'; import SessionData from '../../models/usersessiondata'; import CustomFields from '../../models/customFields'; +import { + OPERATOR_BOARD, + OPERATOR_DUE, OPERATOR_LIST, OPERATOR_SWIMLANE, OPERATOR_USER, + ORDER_ASCENDING, + PREDICATE_ALL, + PREDICATE_ARCHIVED, + PREDICATE_ASSIGNEES, + PREDICATE_ATTACHMENT, + PREDICATE_CHECKLIST, + PREDICATE_CREATED_AT, + PREDICATE_DESCRIPTION, + PREDICATE_DUE_AT, + PREDICATE_END_AT, + PREDICATE_ENDED, + PREDICATE_MEMBERS, + PREDICATE_MODIFIED_AT, + PREDICATE_PRIVATE, + PREDICATE_PUBLIC, + PREDICATE_START_AT, + PREDICATE_SYSTEM, +} from '../../config/search-const'; const escapeForRegex = require('escape-string-regexp'); @@ -17,12 +38,8 @@ Meteor.publish('card', cardId => { }); Meteor.publish('myCards', function(sessionId) { - const queryParams = { - users: [Meteor.user().username], - // limit: 25, - skip: 0, - // sort: { name: 'dueAt', order: 'des' }, - }; + const queryParams = {} + queryParams[OPERATOR_USER] = [Meteor.user().username]; return findCards(sessionId, buildQuery(queryParams)); }); @@ -61,16 +78,20 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { class QueryErrors { constructor() { this.notFound = { - boards: [], - swimlanes: [], - lists: [], + // boards: [], + // swimlanes: [], + // lists: [], labels: [], - users: [], + // 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(); } @@ -87,13 +108,13 @@ class QueryErrors { errorMessages() { const messages = []; - this.notFound.boards.forEach(board => { + this.notFound[OPERATOR_BOARD].forEach(board => { messages.push({ tag: 'board-title-not-found', value: board }); }); - this.notFound.swimlanes.forEach(swim => { + this.notFound[OPERATOR_SWIMLANE].forEach(swim => { messages.push({ tag: 'swimlane-title-not-found', value: swim }); }); - this.notFound.lists.forEach(list => { + this.notFound[OPERATOR_LIST].forEach(list => { messages.push({ tag: 'list-title-not-found', value: list }); }); this.notFound.comments.forEach(comments => { @@ -116,7 +137,7 @@ class QueryErrors { }); } }); - this.notFound.users.forEach(user => { + this.notFound[OPERATOR_USER].forEach(user => { messages.push({ tag: 'user-username-not-found', value: user }); }); this.notFound.members.forEach(user => { @@ -163,13 +184,13 @@ function buildSelector(queryParams) { let endAt = null; if (queryParams.status && queryParams.status.length) { queryParams.status.forEach(status => { - if (status === 'archived') { + if (status === PREDICATE_ARCHIVED) { archived = true; - } else if (status === 'all') { + } else if (status === PREDICATE_ALL) { archived = null; - } else if (status === 'ended') { + } else if (status === PREDICATE_ENDED) { endAt = { $nin: [null, ''] }; - } else if (['private', 'public'].includes(status)) { + } else if ([PREDICATE_PRIVATE, PREDICATE_PUBLIC].includes(status)) { boardsSelector.permission = status; } }); @@ -214,9 +235,9 @@ function buildSelector(queryParams) { selector.endAt = endAt; } - if (queryParams.boards && queryParams.boards.length) { + if (queryParams[OPERATOR_BOARD] && queryParams[OPERATOR_BOARD].length) { const queryBoards = []; - queryParams.boards.forEach(query => { + queryParams[OPERATOR_BOARD].forEach(query => { const boards = Boards.userSearch(userId, { title: new RegExp(escapeForRegex(query), 'i'), }); @@ -225,16 +246,19 @@ function buildSelector(queryParams) { queryBoards.push(board._id); }); } else { - errors.notFound.boards.push(query); + errors.notFound[OPERATOR_BOARD].push(query); } }); selector.boardId.$in = queryBoards; } - if (queryParams.swimlanes && queryParams.swimlanes.length) { + if ( + queryParams[OPERATOR_SWIMLANE] && + queryParams[OPERATOR_SWIMLANE].length + ) { const querySwimlanes = []; - queryParams.swimlanes.forEach(query => { + queryParams[OPERATOR_SWIMLANE].forEach(query => { const swimlanes = Swimlanes.find({ title: new RegExp(escapeForRegex(query), 'i'), }); @@ -243,7 +267,7 @@ function buildSelector(queryParams) { querySwimlanes.push(swim._id); }); } else { - errors.notFound.swimlanes.push(query); + errors.notFound[OPERATOR_SWIMLANE].push(query); } }); @@ -254,9 +278,9 @@ function buildSelector(queryParams) { selector.swimlaneId.$in = querySwimlanes; } - if (queryParams.lists && queryParams.lists.length) { + if (queryParams[OPERATOR_LIST] && queryParams[OPERATOR_LIST].length) { const queryLists = []; - queryParams.lists.forEach(query => { + queryParams[OPERATOR_LIST].forEach(query => { const lists = Lists.find({ title: new RegExp(escapeForRegex(query), 'i'), }); @@ -265,7 +289,7 @@ function buildSelector(queryParams) { queryLists.push(list._id); }); } else { - errors.notFound.lists.push(query); + errors.notFound[OPERATOR_LIST].push(query); } }); @@ -289,7 +313,7 @@ function buildSelector(queryParams) { } } - ['dueAt', 'createdAt', 'modifiedAt'].forEach(field => { + [OPERATOR_DUE, 'createdAt', 'modifiedAt'].forEach(field => { if (queryParams[field]) { selector[field] = {}; selector[field][queryParams[field].operator] = new Date( @@ -302,8 +326,8 @@ function buildSelector(queryParams) { members: [], assignees: [], }; - if (queryParams.users && queryParams.users.length) { - queryParams.users.forEach(query => { + if (queryParams[OPERATOR_USER] && queryParams[OPERATOR_USER].length) { + queryParams[OPERATOR_USER].forEach(query => { const users = Users.find({ username: query, }); @@ -313,7 +337,7 @@ function buildSelector(queryParams) { queryUsers.assignees.push(user._id); }); } else { - errors.notFound.users.push(query); + errors.notFound[OPERATOR_USER].push(query); } }); } @@ -405,7 +429,7 @@ function buildSelector(queryParams) { if (queryParams.has && queryParams.has.length) { queryParams.has.forEach(has => { switch (has.field) { - case 'attachment': + case PREDICATE_ATTACHMENT: selector.$and.push({ _id: { $in: Attachments.find({}, { fields: { cardId: 1 } }).map( @@ -414,7 +438,7 @@ function buildSelector(queryParams) { }, }); break; - case 'checklist': + case PREDICATE_CHECKLIST: selector.$and.push({ _id: { $in: Checklists.find({}, { fields: { cardId: 1 } }).map( @@ -423,18 +447,18 @@ function buildSelector(queryParams) { }, }); break; - case 'description': - case 'startAt': - case 'dueAt': - case 'endAt': + case PREDICATE_DESCRIPTION: + case PREDICATE_START_AT: + case PREDICATE_DUE_AT: + case PREDICATE_END_AT: if (has.exists) { selector[has.field] = { $exists: true, $nin: [null, ''] }; } else { selector[has.field] = { $in: [null, ''] }; } break; - case 'assignees': - case 'members': + case PREDICATE_ASSIGNEES: + case PREDICATE_MEMBERS: if (has.exists) { selector[has.field] = { $exists: true, $nin: [null, []] }; } else { @@ -546,9 +570,9 @@ function buildProjection(query) { }; if (query.params.sort) { - const order = query.params.sort.order === 'asc' ? 1 : -1; + const order = query.params.sort.order === ORDER_ASCENDING ? 1 : -1; switch (query.params.sort.name) { - case 'dueAt': + case PREDICATE_DUE_AT: projection.sort = { dueAt: order, boardId: 1, @@ -557,7 +581,7 @@ function buildProjection(query) { sort: 1, }; break; - case 'modifiedAt': + case PREDICATE_MODIFIED_AT: projection.sort = { modifiedAt: order, boardId: 1, @@ -566,7 +590,7 @@ function buildProjection(query) { sort: 1, }; break; - case 'createdAt': + case PREDICATE_CREATED_AT: projection.sort = { createdAt: order, boardId: 1, @@ -575,7 +599,7 @@ function buildProjection(query) { sort: 1, }; break; - case 'system': + case PREDICATE_SYSTEM: projection.sort = { boardId: order, swimlaneId: order, @@ -602,18 +626,15 @@ function buildQuery(queryParams) { } Meteor.publish('brokenCards', function(sessionId) { - const queryParams = { - users: [Meteor.user().username], - // limit: 25, - skip: 0, - // sort: { name: 'dueAt', order: 'des' }, - }; - const query = buildQuery(queryParams); + check(sessionId, String); + + const query = buildQuery({ status: [PREDICATE_ALL] }); query.selector.$or = [ { boardId: { $in: [null, ''] } }, { swimlaneId: { $in: [null, ''] } }, { listId: { $in: [null, ''] } }, ]; + console.log('brokenCards selector:', query.selector); return findCards(sessionId, query); }); From ba00311dd4449bcee8eba653e451efd72fe4155e Mon Sep 17 00:00:00 2001 From: John Supplee <john@supplee.com> Date: Tue, 9 Mar 2021 02:21:43 +0200 Subject: [PATCH 09/17] Add more constants and convert params object to a class --- client/components/main/globalSearch.js | 92 ++++++----- client/components/main/myCards.js | 9 +- config/query-classes.js | 120 ++++++++++++++ config/search-const.js | 6 + server/publications/cards.js | 216 ++++++++----------------- 5 files changed, 248 insertions(+), 195 deletions(-) create mode 100644 config/query-classes.js 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, ''] } }, From 097cae1f8cea9a2737f73362248866c52737aafb Mon Sep 17 00:00:00 2001 From: "John R. Supplee" <john@supplee.com> Date: Tue, 9 Mar 2021 19:30:04 +0200 Subject: [PATCH 10/17] More integration with constants and query classes --- client/components/main/globalSearch.jade | 4 +- client/components/main/globalSearch.js | 90 ++++++++---------- config/query-classes.js | 115 +++++++++++++++-------- config/search-const.js | 5 + server/publications/cards.js | 55 ++++++----- 5 files changed, 153 insertions(+), 116 deletions(-) diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 5389fb780..47fba719d 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -44,9 +44,9 @@ template(name="globalSearch") else if hasResults.get .global-search-results-list-wrapper if hasQueryErrors.get - div + ul each msg in errorMessages - span.global-search-error-messages + li.global-search-error-messages = msg else +resultsPaged(this) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index e8b6870ac..22049cc12 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -1,16 +1,22 @@ import { CardSearchPagedComponent } from '../../lib/cardSearch'; +import Boards from '../../../models/boards'; import moment from 'moment'; import { OPERATOR_ASSIGNEE, OPERATOR_BOARD, + OPERATOR_COMMENT, + OPERATOR_CREATED_AT, OPERATOR_DUE, OPERATOR_HAS, OPERATOR_LABEL, + OPERATOR_LIMIT, OPERATOR_LIST, OPERATOR_MEMBER, + OPERATOR_MODIFIED_AT, OPERATOR_SORT, OPERATOR_STATUS, OPERATOR_SWIMLANE, + OPERATOR_UNKNOWN, OPERATOR_USER, ORDER_ASCENDING, ORDER_DESCENDING, @@ -36,7 +42,7 @@ import { PREDICATE_WEEK, PREDICATE_YEAR, } from '../../../config/search-const'; -import { QueryParams } from "../../../config/query-classes"; +import { QueryErrors, QueryParams } from '../../../config/query-classes'; // const subManager = new SubsManager(); @@ -80,7 +86,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { this.myLists = new ReactiveVar([]); this.myLabelNames = new ReactiveVar([]); this.myBoardNames = new ReactiveVar([]); - this.parsingErrors = []; + this.parsingErrors = new QueryErrors(); this.colorMap = null; this.queryParams = null; @@ -119,26 +125,18 @@ class GlobalSearchComponent extends CardSearchPagedComponent { resetSearch() { super.resetSearch(); - this.parsingErrors = []; + this.parsingErrors = new QueryErrors(); } errorMessages() { - if (this.parsingErrors.length) { - return this.parsingErrorMessages(); + if (this.parsingErrors.hasErrors()) { + return this.parsingErrors.errorMessages(); } return this.queryErrorMessages(); } parsingErrorMessages() { - const messages = []; - - if (this.parsingErrors.length) { - this.parsingErrors.forEach(err => { - messages.push(TAPi18n.__(err.tag, err.value)); - }); - } - - return messages; + this.parsingErrors.errorMessages(); } searchAllBoards(query) { @@ -188,12 +186,12 @@ class GlobalSearchComponent extends CardSearchPagedComponent { 'operator-assignee-abbrev': OPERATOR_ASSIGNEE, 'operator-status': OPERATOR_STATUS, 'operator-due': OPERATOR_DUE, - 'operator-created': 'createdAt', - 'operator-modified': 'modifiedAt', - 'operator-comment': 'comments', + 'operator-created': OPERATOR_CREATED_AT, + 'operator-modified': OPERATOR_MODIFIED_AT, + 'operator-comment': OPERATOR_COMMENT, 'operator-has': OPERATOR_HAS, 'operator-sort': OPERATOR_SORT, - 'operator-limit': 'limit', + 'operator-limit': OPERATOR_LIMIT, }; const predicates = { @@ -247,29 +245,6 @@ 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_LABEL] = []; - // params[OPERATOR_LIST] = []; - // params[OPERATOR_SWIMLANE] = []; - // params[OPERATOR_USER] = []; - const params = new QueryParams(); let text = ''; while (query) { @@ -299,7 +274,9 @@ class GlobalSearchComponent extends CardSearchPagedComponent { // console.log('found color:', value); } } else if ( - [OPERATOR_DUE, 'createdAt', 'modifiedAt'].includes(operator) + [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].includes( + operator, + ) ) { const days = parseInt(value, 10); let duration = null; @@ -350,19 +327,22 @@ class GlobalSearchComponent extends CardSearchPagedComponent { value: date.format('YYYY-MM-DD'), }; } - } else if (operator === 'dueAt' && value === PREDICATE_OVERDUE) { + } else if ( + operator === OPERATOR_DUE && + value === PREDICATE_OVERDUE + ) { value = { operator: '$lt', value: moment().format('YYYY-MM-DD'), }; } else { - this.parsingErrors.push({ + this.parsingErrors.addError(OPERATOR_DUE, { tag: 'operator-number-expected', value: { operator: op, value }, }); - value = null; + continue; } - } else if (operator === 'dueAt') { + } else if (operator === OPERATOR_DUE) { value = { operator: '$lt', value: moment(moment().format('YYYY-MM-DD')) @@ -385,10 +365,11 @@ class GlobalSearchComponent extends CardSearchPagedComponent { negated = true; } if (!predicateTranslations.sorts[value]) { - this.parsingErrors.push({ + this.parsingErrors.addError(OPERATOR_SORT, { tag: 'operator-sort-invalid', value, }); + continue; } else { value = { name: predicateTranslations.sorts[value], @@ -397,10 +378,11 @@ class GlobalSearchComponent extends CardSearchPagedComponent { } } else if (operator === OPERATOR_STATUS) { if (!predicateTranslations.status[value]) { - this.parsingErrors.push({ + this.parsingErrors.addError(OPERATOR_STATUS, { tag: 'operator-status-invalid', value, }); + continue; } else { value = predicateTranslations.status[value]; } @@ -412,23 +394,25 @@ class GlobalSearchComponent extends CardSearchPagedComponent { negated = true; } if (!predicateTranslations.has[value]) { - this.parsingErrors.push({ + this.parsingErrors.addError(OPERATOR_HAS, { tag: 'operator-has-invalid', value, }); + continue; } else { value = { field: predicateTranslations.has[value], exists: !negated, }; } - } else if (operator === 'limit') { + } else if (operator === OPERATOR_LIMIT) { const limit = parseInt(value, 10); if (isNaN(limit) || limit < 1) { - this.parsingErrors.push({ + this.parsingErrors.addError(OPERATOR_LIMIT, { tag: 'operator-limit-invalid', value, }); + continue; } else { value = limit; } @@ -436,7 +420,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { params.addPredicate(operator, value); } else { - this.parsingErrors.push({ + this.parsingErrors.addError(OPERATOR_UNKNOWN, { tag: 'operator-unknown-error', value: op, }); @@ -467,7 +451,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { this.queryParams = params; - if (this.parsingErrors.length) { + if (this.parsingErrors.hasErrors()) { this.searching.set(false); this.queryErrors = this.parsingErrorMessages(); this.hasResults.set(true); diff --git a/config/query-classes.js b/config/query-classes.js index f76109f45..ea597455e 100644 --- a/config/query-classes.js +++ b/config/query-classes.js @@ -8,9 +8,9 @@ import { OPERATOR_SWIMLANE, OPERATOR_USER, } from './search-const'; +import Boards from '../models/boards'; export class QueryParams { - text = ''; constructor(params = {}) { @@ -46,58 +46,81 @@ export class QueryParams { } export class QueryErrors { + operatorTagMap = [ + [OPERATOR_BOARD, 'board-title-not-found'], + [OPERATOR_SWIMLANE, 'swimlane-title-not-found'], + [ + OPERATOR_LABEL, + label => { + if (Boards.labelColors().includes(label)) { + return { + tag: 'label-color-not-found', + value: label, + color: true, + }; + } else { + return { + tag: 'label-not-found', + value: label, + color: false, + }; + } + }, + ], + [OPERATOR_LIST, 'list-title-not-found'], + [OPERATOR_COMMENT, 'comment-not-found'], + [OPERATOR_USER, 'user-username-not-found'], + [OPERATOR_ASSIGNEE, 'user-username-not-found'], + [OPERATOR_MEMBER, 'user-username-not-found'], + ]; + constructor() { - this.errors = {}; + this._errors = {}; + + this.operatorTags = {}; + this.operatorTagMap.forEach(([operator, tag]) => { + this.operatorTags[operator] = tag; + }); this.colorMap = Boards.colorMap(); } - addError(operator, value) { - if (!this.errors[operator]) { - this.errors[operator] = []; + addError(operator, error) { + if (!this._errors[operator]) { + this._errors[operator] = []; + } + this._errors[operator].push(error); + } + + addNotFound(operator, value) { + if (typeof this.operatorTags[operator] === 'function') { + this.addError(operator, this.operatorTags[operator](value)); + } else { + this.addError(operator, { tag: this.operatorTags[operator], value }); } - this.errors[operator].push(value) } hasErrors() { - return Object.entries(this.errors).length > 0; + return Object.entries(this._errors).length > 0; + } + + errors() { + const errs = []; + Object.entries(this._errors).forEach(([operator, errors]) => { + errors.forEach(err => { + errs.push(err); + }); + }); + return errs; } 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 }); - } + Object.entries(this._errors).forEach(([operator, errors]) => { + errors.forEach(err => { + messages.push(TAPi18n.__(err.tag, err.value)); + }); }); - return messages; } } @@ -106,9 +129,9 @@ export class Query { params = {}; selector = {}; projection = {}; - errors = new QueryErrors(); constructor(selector, projection) { + this._errors = new QueryErrors(); if (selector) { this.selector = selector; } @@ -117,4 +140,16 @@ export class Query { this.projection = projection; } } + + hasErrors() { + return this._errors.hasErrors(); + } + + errors() { + return this._errors.errors(); + } + + errorMessages() { + return this._errors.errorMessages(); + } } diff --git a/config/search-const.js b/config/search-const.js index fe16bee0e..26f8ad00b 100644 --- a/config/search-const.js +++ b/config/search-const.js @@ -1,14 +1,19 @@ +export const DEFAULT_LIMIT = 25; export const OPERATOR_ASSIGNEE = 'assignee'; export const OPERATOR_COMMENT = 'comment'; +export const OPERATOR_CREATED_AT = 'createdAt'; export const OPERATOR_DUE = 'dueAt'; export const OPERATOR_BOARD = 'board'; export const OPERATOR_HAS = 'has'; export const OPERATOR_LABEL = 'label'; +export const OPERATOR_LIMIT = 'limit'; export const OPERATOR_LIST = 'list'; export const OPERATOR_MEMBER = 'member'; +export const OPERATOR_MODIFIED_AT = 'modifiedAt'; export const OPERATOR_SORT = 'sort'; export const OPERATOR_STATUS = 'status'; export const OPERATOR_SWIMLANE = 'swimlane'; +export const OPERATOR_UNKNOWN = 'unknown'; export const OPERATOR_USER = 'user'; export const ORDER_ASCENDING = 'asc'; export const ORDER_DESCENDING = 'des'; diff --git a/server/publications/cards.js b/server/publications/cards.js index fc226b538..dfdf195e2 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -9,12 +9,14 @@ import ChecklistItems from '../../models/checklistItems'; import SessionData from '../../models/usersessiondata'; import CustomFields from '../../models/customFields'; import { + DEFAULT_LIMIT, OPERATOR_ASSIGNEE, OPERATOR_BOARD, OPERATOR_COMMENT, OPERATOR_DUE, OPERATOR_HAS, OPERATOR_LABEL, + OPERATOR_LIMIT, OPERATOR_LIST, OPERATOR_MEMBER, OPERATOR_SORT, @@ -81,7 +83,7 @@ Meteor.publish('globalSearch', function(sessionId, params) { check(params, Object); // eslint-disable-next-line no-console - // console.log('queryParams:', queryParams); + console.log('queryParams:', params); return findCards(sessionId, buildQuery(new QueryParams(params))); }); @@ -164,7 +166,7 @@ function buildSelector(queryParams) { queryBoards.push(board._id); }); } else { - errors.addError(OPERATOR_BOARD, query); + errors.addNotFound(OPERATOR_BOARD, query); } }); @@ -182,7 +184,7 @@ function buildSelector(queryParams) { querySwimlanes.push(swim._id); }); } else { - errors.addError(OPERATOR_SWIMLANE, query); + errors.addNotFound(OPERATOR_SWIMLANE, query); } }); @@ -204,7 +206,7 @@ function buildSelector(queryParams) { queryLists.push(list._id); }); } else { - errors.addError(OPERATOR_LIST, query); + errors.addNotFound(OPERATOR_LIST, query); } }); @@ -216,7 +218,9 @@ function buildSelector(queryParams) { } if (queryParams.hasOperator(OPERATOR_COMMENT)) { - const cardIds = CardComments.textSearch(userId, queryParams.getPredicates(OPERATOR_COMMENT)).map( + const cardIds = CardComments.textSearch( + userId, + queryParams.getPredicates(OPERATOR_COMMENT), com => { return com.cardId; }, @@ -225,7 +229,7 @@ function buildSelector(queryParams) { selector._id = { $in: cardIds }; } else { queryParams.getPredicates(OPERATOR_COMMENT).forEach(comment => { - errors.addError(OPERATOR_COMMENT, comment); + errors.addNotFound(OPERATOR_COMMENT, comment); }); } } @@ -238,7 +242,7 @@ function buildSelector(queryParams) { } }); - const queryUsers = {} + const queryUsers = {}; queryUsers[OPERATOR_ASSIGNEE] = []; queryUsers[OPERATOR_MEMBER] = []; @@ -253,7 +257,7 @@ function buildSelector(queryParams) { queryUsers[OPERATOR_ASSIGNEE].push(user._id); }); } else { - errors.addError(OPERATOR_USER, query); + errors.addNotFound(OPERATOR_USER, query); } }); } @@ -269,13 +273,16 @@ function buildSelector(queryParams) { queryUsers[key].push(user._id); }); } else { - errors.addError(key, query); + errors.addNotFound(key, query); } }); } }); - if (queryUsers[OPERATOR_MEMBER].length && queryUsers[OPERATOR_ASSIGNEE].length) { + if ( + queryUsers[OPERATOR_MEMBER].length && + queryUsers[OPERATOR_ASSIGNEE].length + ) { selector.$and.push({ $or: [ { members: { $in: queryUsers[OPERATOR_MEMBER] } }, @@ -334,7 +341,7 @@ function buildSelector(queryParams) { }); }); } else { - errors.addError(OPERATOR_LABEL, label); + errors.addNotFound(OPERATOR_LABEL, label); } } @@ -441,7 +448,7 @@ function buildSelector(queryParams) { const query = new Query(); query.selector = selector; query.params = queryParams; - query.errors = errors; + query._errors = errors; return query; } @@ -451,9 +458,9 @@ function buildProjection(query) { if (query.params.skip) { skip = query.params.skip; } - let limit = 25; - if (query.params.limit) { - limit = query.params.limit; + let limit = DEFAULT_LIMIT; + if (query.params.hasOperator(OPERATOR_LIMIT)) { + limit = query.params.getPredicate(OPERATOR_LIMIT); } const projection = { @@ -485,9 +492,12 @@ function buildProjection(query) { limit, }; - if (query.params[OPERATOR_SORT]) { - const order = query.params[OPERATOR_SORT].order === ORDER_ASCENDING ? 1 : -1; - switch (query.params[OPERATOR_SORT].name) { + if (query.params.hasOperator(OPERATOR_SORT)) { + const order = + query.params.getPredicate(OPERATOR_SORT).order === ORDER_ASCENDING + ? 1 + : -1; + switch (query.params.getPredicate(OPERATOR_SORT).name) { case PREDICATE_DUE_AT: projection.sort = { dueAt: order, @@ -586,12 +596,13 @@ function findCards(sessionId, query) { // eslint-disable-next-line no-console // console.log('projection:', projection); let cards; - if (!query.errors || !query.errors.hasErrors()) { + if (!query.hasErrors()) { cards = Cards.find(query.selector, query.projection); } // eslint-disable-next-line no-console // console.log('count:', cards.count()); + console.log(query); const update = { $set: { totalHits: 0, @@ -600,13 +611,15 @@ function findCards(sessionId, query) { cards: [], selector: SessionData.pickle(query.selector), projection: SessionData.pickle(query.projection), - errors: query.errors.errorMessages(), + errors: query.errors(), }, }; // if (errors) { - // update.$set.errors = errors.errorMessages(); + // update.$set.errors = errors.errors(); // } + console.log('errors:', query.errors()); + if (cards) { update.$set.totalHits = cards.count(); update.$set.lastHit = From 6def7d6f7009ffe55439e03b211d0904f666c42c Mon Sep 17 00:00:00 2001 From: "John R. Supplee" <john@supplee.com> Date: Wed, 10 Mar 2021 12:39:39 +0200 Subject: [PATCH 11/17] Move query parsing to Query class --- client/components/main/globalSearch.js | 379 +------------------------ config/query-classes.js | 350 ++++++++++++++++++++++- server/publications/cards.js | 26 +- 3 files changed, 374 insertions(+), 381 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 22049cc12..294b3dace 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -1,48 +1,6 @@ import { CardSearchPagedComponent } from '../../lib/cardSearch'; import Boards from '../../../models/boards'; -import moment from 'moment'; -import { - OPERATOR_ASSIGNEE, - OPERATOR_BOARD, - OPERATOR_COMMENT, - OPERATOR_CREATED_AT, - OPERATOR_DUE, - OPERATOR_HAS, - OPERATOR_LABEL, - OPERATOR_LIMIT, - OPERATOR_LIST, - OPERATOR_MEMBER, - OPERATOR_MODIFIED_AT, - OPERATOR_SORT, - OPERATOR_STATUS, - OPERATOR_SWIMLANE, - OPERATOR_UNKNOWN, - OPERATOR_USER, - ORDER_ASCENDING, - ORDER_DESCENDING, - PREDICATE_ALL, - PREDICATE_ARCHIVED, - PREDICATE_ASSIGNEES, - PREDICATE_ATTACHMENT, - PREDICATE_CHECKLIST, - PREDICATE_CREATED_AT, - PREDICATE_DESCRIPTION, - PREDICATE_DUE_AT, - PREDICATE_END_AT, - PREDICATE_ENDED, - PREDICATE_MEMBERS, - PREDICATE_MODIFIED_AT, - PREDICATE_MONTH, - PREDICATE_OPEN, - PREDICATE_OVERDUE, - PREDICATE_PRIVATE, - PREDICATE_PUBLIC, - PREDICATE_QUARTER, - PREDICATE_START_AT, - PREDICATE_WEEK, - PREDICATE_YEAR, -} from '../../../config/search-const'; -import { QueryErrors, QueryParams } from '../../../config/query-classes'; +import { Query, QueryErrors } from '../../../config/query-classes'; // const subManager = new SubsManager(); @@ -62,24 +20,6 @@ Template.globalSearch.helpers({ }, }); -BlazeComponent.extendComponent({ - events() { - return [ - { - 'click .js-due-cards-view-me'() { - Utils.setDueCardsView('me'); - Popup.close(); - }, - - 'click .js-due-cards-view-all'() { - Utils.setDueCardsView('all'); - Popup.close(); - }, - }, - ]; - }, -}).register('globalSearchViewChangePopup'); - class GlobalSearchComponent extends CardSearchPagedComponent { onCreated() { super.onCreated(); @@ -87,7 +27,6 @@ class GlobalSearchComponent extends CardSearchPagedComponent { this.myLabelNames = new ReactiveVar([]); this.myBoardNames = new ReactiveVar([]); this.parsingErrors = new QueryErrors(); - this.colorMap = null; this.queryParams = null; Meteor.call('myLists', (err, data) => { @@ -114,9 +53,6 @@ class GlobalSearchComponent extends CardSearchPagedComponent { // eslint-disable-next-line no-console //console.log('lang:', TAPi18n.getLanguage()); - this.colorMap = Boards.colorMap(); - // eslint-disable-next-line no-console - // console.log('colorMap:', this.colorMap); if (Session.get('globalQuery')) { this.searchAllBoards(Session.get('globalQuery')); @@ -139,327 +75,38 @@ class GlobalSearchComponent extends CardSearchPagedComponent { this.parsingErrors.errorMessages(); } - searchAllBoards(query) { - query = query.trim(); + searchAllBoards(queryText) { + queryText = queryText.trim(); // eslint-disable-next-line no-console - //console.log('query:', query); + //console.log('queryText:', queryText); - this.query.set(query); + this.query.set(queryText); this.resetSearch(); - if (!query) { + if (!queryText) { return; } this.searching.set(true); - const reOperator1 = new RegExp( - '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<value>[\\p{Letter}\\p{Mark}]+)(\\s+|$)', - 'iu', - ); - const reOperator2 = new RegExp( - '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<quote>["\']*)(?<value>.*?)\\k<quote>(\\s+|$)', - 'iu', - ); - const reText = new RegExp('^(?<text>\\S+)(\\s+|$)', 'u'); - const reQuotedText = new RegExp( - '^(?<quote>["\'])(?<text>.*?)\\k<quote>(\\s+|$)', - 'u', - ); - const reNegatedOperator = new RegExp('^-(?<operator>.*)$'); - - const operators = { - 'operator-board': OPERATOR_BOARD, - 'operator-board-abbrev': OPERATOR_BOARD, - 'operator-swimlane': OPERATOR_SWIMLANE, - 'operator-swimlane-abbrev': OPERATOR_SWIMLANE, - 'operator-list': OPERATOR_LIST, - 'operator-list-abbrev': OPERATOR_LIST, - 'operator-label': OPERATOR_LABEL, - 'operator-label-abbrev': OPERATOR_LABEL, - 'operator-user': OPERATOR_USER, - 'operator-user-abbrev': OPERATOR_USER, - 'operator-member': OPERATOR_MEMBER, - 'operator-member-abbrev': OPERATOR_MEMBER, - 'operator-assignee': OPERATOR_ASSIGNEE, - 'operator-assignee-abbrev': OPERATOR_ASSIGNEE, - 'operator-status': OPERATOR_STATUS, - 'operator-due': OPERATOR_DUE, - 'operator-created': OPERATOR_CREATED_AT, - 'operator-modified': OPERATOR_MODIFIED_AT, - 'operator-comment': OPERATOR_COMMENT, - 'operator-has': OPERATOR_HAS, - 'operator-sort': OPERATOR_SORT, - 'operator-limit': OPERATOR_LIMIT, - }; - - const predicates = { - due: { - 'predicate-overdue': PREDICATE_OVERDUE, - }, - durations: { - 'predicate-week': PREDICATE_WEEK, - 'predicate-month': PREDICATE_MONTH, - 'predicate-quarter': PREDICATE_QUARTER, - 'predicate-year': PREDICATE_YEAR, - }, - status: { - 'predicate-archived': PREDICATE_ARCHIVED, - 'predicate-all': PREDICATE_ALL, - 'predicate-open': PREDICATE_OPEN, - 'predicate-ended': PREDICATE_ENDED, - 'predicate-public': PREDICATE_PUBLIC, - 'predicate-private': PREDICATE_PRIVATE, - }, - sorts: { - 'predicate-due': PREDICATE_DUE_AT, - 'predicate-created': PREDICATE_CREATED_AT, - 'predicate-modified': PREDICATE_MODIFIED_AT, - }, - has: { - 'predicate-description': PREDICATE_DESCRIPTION, - 'predicate-checklist': PREDICATE_CHECKLIST, - 'predicate-attachment': PREDICATE_ATTACHMENT, - 'predicate-start': PREDICATE_START_AT, - 'predicate-end': PREDICATE_END_AT, - 'predicate-due': PREDICATE_DUE_AT, - 'predicate-assignee': PREDICATE_ASSIGNEES, - 'predicate-member': PREDICATE_MEMBERS, - }, - }; - const predicateTranslations = {}; - Object.entries(predicates).forEach(([category, catPreds]) => { - predicateTranslations[category] = {}; - Object.entries(catPreds).forEach(([tag, value]) => { - predicateTranslations[category][TAPi18n.__(tag)] = value; - }); - }); - // eslint-disable-next-line no-console - // console.log('predicateTranslations:', predicateTranslations); - - const operatorMap = {}; - Object.entries(operators).forEach(([key, value]) => { - operatorMap[TAPi18n.__(key).toLowerCase()] = value; - }); - // eslint-disable-next-line no-console - // console.log('operatorMap:', operatorMap); - - const params = new QueryParams(); - let text = ''; - while (query) { - let m = query.match(reOperator1); - if (!m) { - m = query.match(reOperator2); - if (m) { - query = query.replace(reOperator2, ''); - } - } else { - query = query.replace(reOperator1, ''); - } - if (m) { - let op; - if (m.groups.operator) { - op = m.groups.operator.toLowerCase(); - } else { - op = m.groups.abbrev.toLowerCase(); - } - // eslint-disable-next-line no-prototype-builtins - if (operatorMap.hasOwnProperty(op)) { - const operator = operatorMap[op]; - let value = m.groups.value; - if (operator === OPERATOR_LABEL) { - if (value in this.colorMap) { - value = this.colorMap[value]; - // console.log('found color:', value); - } - } else if ( - [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].includes( - operator, - ) - ) { - const days = parseInt(value, 10); - let duration = null; - if (isNaN(days)) { - // duration was specified as text - if (predicateTranslations.durations[value]) { - duration = predicateTranslations.durations[value]; - let date = null; - switch (duration) { - case PREDICATE_WEEK: - // eslint-disable-next-line no-case-declarations - const week = moment().week(); - if (week === 52) { - date = moment(1, 'W'); - date.set('year', date.year() + 1); - } else { - date = moment(week + 1, 'W'); - } - break; - case PREDICATE_MONTH: - // eslint-disable-next-line no-case-declarations - const month = moment().month(); - // .month() is zero indexed - if (month === 11) { - date = moment(1, 'M'); - date.set('year', date.year() + 1); - } else { - date = moment(month + 2, 'M'); - } - break; - case PREDICATE_QUARTER: - // eslint-disable-next-line no-case-declarations - const quarter = moment().quarter(); - if (quarter === 4) { - date = moment(1, 'Q'); - date.set('year', date.year() + 1); - } else { - date = moment(quarter + 1, 'Q'); - } - break; - case PREDICATE_YEAR: - date = moment(moment().year() + 1, 'YYYY'); - break; - } - if (date) { - value = { - operator: '$lt', - value: date.format('YYYY-MM-DD'), - }; - } - } else if ( - operator === OPERATOR_DUE && - value === PREDICATE_OVERDUE - ) { - value = { - operator: '$lt', - value: moment().format('YYYY-MM-DD'), - }; - } else { - this.parsingErrors.addError(OPERATOR_DUE, { - tag: 'operator-number-expected', - value: { operator: op, value }, - }); - continue; - } - } else if (operator === OPERATOR_DUE) { - value = { - operator: '$lt', - value: moment(moment().format('YYYY-MM-DD')) - .add(days + 1, duration ? duration : 'days') - .format(), - }; - } else { - value = { - operator: '$gte', - value: moment(moment().format('YYYY-MM-DD')) - .subtract(days, duration ? duration : 'days') - .format(), - }; - } - } else if (operator === OPERATOR_SORT) { - let negated = false; - const m = value.match(reNegatedOperator); - if (m) { - value = m.groups.operator; - negated = true; - } - if (!predicateTranslations.sorts[value]) { - this.parsingErrors.addError(OPERATOR_SORT, { - tag: 'operator-sort-invalid', - value, - }); - continue; - } else { - value = { - name: predicateTranslations.sorts[value], - order: negated ? ORDER_DESCENDING : ORDER_ASCENDING, - }; - } - } else if (operator === OPERATOR_STATUS) { - if (!predicateTranslations.status[value]) { - this.parsingErrors.addError(OPERATOR_STATUS, { - tag: 'operator-status-invalid', - value, - }); - continue; - } else { - value = predicateTranslations.status[value]; - } - } else if (operator === OPERATOR_HAS) { - let negated = false; - const m = value.match(reNegatedOperator); - if (m) { - value = m.groups.operator; - negated = true; - } - if (!predicateTranslations.has[value]) { - this.parsingErrors.addError(OPERATOR_HAS, { - tag: 'operator-has-invalid', - value, - }); - continue; - } else { - value = { - field: predicateTranslations.has[value], - exists: !negated, - }; - } - } else if (operator === OPERATOR_LIMIT) { - const limit = parseInt(value, 10); - if (isNaN(limit) || limit < 1) { - this.parsingErrors.addError(OPERATOR_LIMIT, { - tag: 'operator-limit-invalid', - value, - }); - continue; - } else { - value = limit; - } - } - - params.addPredicate(operator, value); - } else { - this.parsingErrors.addError(OPERATOR_UNKNOWN, { - tag: 'operator-unknown-error', - value: op, - }); - } - continue; - } - - m = query.match(reQuotedText); - if (!m) { - m = query.match(reText); - if (m) { - query = query.replace(reText, ''); - } - } else { - query = query.replace(reQuotedText, ''); - } - if (m) { - text += (text ? ' ' : '') + m.groups.text; - } - } + const query = new Query(); + query.buildParams(queryText); // eslint-disable-next-line no-console - // console.log('text:', text); - params.text = text; + // console.log('params:', query.getParams()); - // eslint-disable-next-line no-console - console.log('params:', params); + this.queryParams = query.getParams(); - this.queryParams = params; - - if (this.parsingErrors.hasErrors()) { + if (query.hasErrors()) { this.searching.set(false); - this.queryErrors = this.parsingErrorMessages(); + this.queryErrors = query.errors(); this.hasResults.set(true); this.hasQueryErrors.set(true); return; } - this.runGlobalSearch(params.getParams()); + this.runGlobalSearch(query.getParams()); } searchInstructions() { diff --git a/config/query-classes.js b/config/query-classes.js index ea597455e..aa97f306a 100644 --- a/config/query-classes.js +++ b/config/query-classes.js @@ -2,13 +2,45 @@ import { OPERATOR_ASSIGNEE, OPERATOR_BOARD, OPERATOR_COMMENT, + OPERATOR_CREATED_AT, + OPERATOR_DUE, + OPERATOR_HAS, OPERATOR_LABEL, + OPERATOR_LIMIT, OPERATOR_LIST, OPERATOR_MEMBER, + OPERATOR_MODIFIED_AT, + OPERATOR_SORT, + OPERATOR_STATUS, OPERATOR_SWIMLANE, + OPERATOR_UNKNOWN, OPERATOR_USER, + ORDER_ASCENDING, + ORDER_DESCENDING, + PREDICATE_ALL, + PREDICATE_ARCHIVED, + PREDICATE_ASSIGNEES, + PREDICATE_ATTACHMENT, + PREDICATE_CHECKLIST, + PREDICATE_CREATED_AT, + PREDICATE_DESCRIPTION, + PREDICATE_DUE_AT, + PREDICATE_END_AT, + PREDICATE_ENDED, + PREDICATE_MEMBERS, + PREDICATE_MODIFIED_AT, + PREDICATE_MONTH, + PREDICATE_OPEN, + PREDICATE_OVERDUE, + PREDICATE_PRIVATE, + PREDICATE_PUBLIC, + PREDICATE_QUARTER, + PREDICATE_START_AT, + PREDICATE_WEEK, + PREDICATE_YEAR, } from './search-const'; import Boards from '../models/boards'; +import moment from 'moment'; export class QueryParams { text = ''; @@ -106,7 +138,8 @@ export class QueryErrors { errors() { const errs = []; - Object.entries(this._errors).forEach(([operator, errors]) => { + // eslint-disable-next-line no-unused-vars + Object.entries(this._errors).forEach(([, errors]) => { errors.forEach(err => { errs.push(err); }); @@ -116,7 +149,8 @@ export class QueryErrors { errorMessages() { const messages = []; - Object.entries(this._errors).forEach(([operator, errors]) => { + // eslint-disable-next-line no-unused-vars + Object.entries(this._errors).forEach(([, errors]) => { errors.forEach(err => { messages.push(TAPi18n.__(err.tag, err.value)); }); @@ -126,12 +160,14 @@ export class QueryErrors { } export class Query { - params = {}; selector = {}; projection = {}; constructor(selector, projection) { this._errors = new QueryErrors(); + this.queryParams = new QueryParams(); + this.colorMap = Boards.colorMap(); + if (selector) { this.selector = selector; } @@ -152,4 +188,312 @@ export class Query { errorMessages() { return this._errors.errorMessages(); } + + getParams() { + return this.queryParams.getParams(); + } + + buildParams(queryText) { + queryText = queryText.trim(); + // eslint-disable-next-line no-console + //console.log('query:', query); + + if (!queryText) { + return; + } + + const reOperator1 = new RegExp( + '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<value>[\\p{Letter}\\p{Mark}]+)(\\s+|$)', + 'iu', + ); + const reOperator2 = new RegExp( + '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<quote>["\']*)(?<value>.*?)\\k<quote>(\\s+|$)', + 'iu', + ); + const reText = new RegExp('^(?<text>\\S+)(\\s+|$)', 'u'); + const reQuotedText = new RegExp( + '^(?<quote>["\'])(?<text>.*?)\\k<quote>(\\s+|$)', + 'u', + ); + const reNegatedOperator = new RegExp('^-(?<operator>.*)$'); + + const operators = { + 'operator-board': OPERATOR_BOARD, + 'operator-board-abbrev': OPERATOR_BOARD, + 'operator-swimlane': OPERATOR_SWIMLANE, + 'operator-swimlane-abbrev': OPERATOR_SWIMLANE, + 'operator-list': OPERATOR_LIST, + 'operator-list-abbrev': OPERATOR_LIST, + 'operator-label': OPERATOR_LABEL, + 'operator-label-abbrev': OPERATOR_LABEL, + 'operator-user': OPERATOR_USER, + 'operator-user-abbrev': OPERATOR_USER, + 'operator-member': OPERATOR_MEMBER, + 'operator-member-abbrev': OPERATOR_MEMBER, + 'operator-assignee': OPERATOR_ASSIGNEE, + 'operator-assignee-abbrev': OPERATOR_ASSIGNEE, + 'operator-status': OPERATOR_STATUS, + 'operator-due': OPERATOR_DUE, + 'operator-created': OPERATOR_CREATED_AT, + 'operator-modified': OPERATOR_MODIFIED_AT, + 'operator-comment': OPERATOR_COMMENT, + 'operator-has': OPERATOR_HAS, + 'operator-sort': OPERATOR_SORT, + 'operator-limit': OPERATOR_LIMIT, + }; + + const predicates = { + due: { + 'predicate-overdue': PREDICATE_OVERDUE, + }, + durations: { + 'predicate-week': PREDICATE_WEEK, + 'predicate-month': PREDICATE_MONTH, + 'predicate-quarter': PREDICATE_QUARTER, + 'predicate-year': PREDICATE_YEAR, + }, + status: { + 'predicate-archived': PREDICATE_ARCHIVED, + 'predicate-all': PREDICATE_ALL, + 'predicate-open': PREDICATE_OPEN, + 'predicate-ended': PREDICATE_ENDED, + 'predicate-public': PREDICATE_PUBLIC, + 'predicate-private': PREDICATE_PRIVATE, + }, + sorts: { + 'predicate-due': PREDICATE_DUE_AT, + 'predicate-created': PREDICATE_CREATED_AT, + 'predicate-modified': PREDICATE_MODIFIED_AT, + }, + has: { + 'predicate-description': PREDICATE_DESCRIPTION, + 'predicate-checklist': PREDICATE_CHECKLIST, + 'predicate-attachment': PREDICATE_ATTACHMENT, + 'predicate-start': PREDICATE_START_AT, + 'predicate-end': PREDICATE_END_AT, + 'predicate-due': PREDICATE_DUE_AT, + 'predicate-assignee': PREDICATE_ASSIGNEES, + 'predicate-member': PREDICATE_MEMBERS, + }, + }; + const predicateTranslations = {}; + Object.entries(predicates).forEach(([category, catPreds]) => { + predicateTranslations[category] = {}; + Object.entries(catPreds).forEach(([tag, value]) => { + predicateTranslations[category][TAPi18n.__(tag)] = value; + }); + }); + // eslint-disable-next-line no-console + // console.log('predicateTranslations:', predicateTranslations); + + const operatorMap = {}; + Object.entries(operators).forEach(([key, value]) => { + operatorMap[TAPi18n.__(key).toLowerCase()] = value; + }); + // eslint-disable-next-line no-console + // console.log('operatorMap:', operatorMap); + + let text = ''; + while (queryText) { + let m = queryText.match(reOperator1); + if (!m) { + m = queryText.match(reOperator2); + if (m) { + queryText = queryText.replace(reOperator2, ''); + } + } else { + queryText = queryText.replace(reOperator1, ''); + } + if (m) { + let op; + if (m.groups.operator) { + op = m.groups.operator.toLowerCase(); + } else { + op = m.groups.abbrev.toLowerCase(); + } + // eslint-disable-next-line no-prototype-builtins + if (operatorMap.hasOwnProperty(op)) { + const operator = operatorMap[op]; + let value = m.groups.value; + if (operator === OPERATOR_LABEL) { + if (value in this.colorMap) { + value = this.colorMap[value]; + // console.log('found color:', value); + } + } else if ( + [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].includes( + operator, + ) + ) { + const days = parseInt(value, 10); + let duration = null; + if (isNaN(days)) { + // duration was specified as text + if (predicateTranslations.durations[value]) { + duration = predicateTranslations.durations[value]; + let date = null; + switch (duration) { + case PREDICATE_WEEK: + // eslint-disable-next-line no-case-declarations + const week = moment().week(); + if (week === 52) { + date = moment(1, 'W'); + date.set('year', date.year() + 1); + } else { + date = moment(week + 1, 'W'); + } + break; + case PREDICATE_MONTH: + // eslint-disable-next-line no-case-declarations + const month = moment().month(); + // .month() is zero indexed + if (month === 11) { + date = moment(1, 'M'); + date.set('year', date.year() + 1); + } else { + date = moment(month + 2, 'M'); + } + break; + case PREDICATE_QUARTER: + // eslint-disable-next-line no-case-declarations + const quarter = moment().quarter(); + if (quarter === 4) { + date = moment(1, 'Q'); + date.set('year', date.year() + 1); + } else { + date = moment(quarter + 1, 'Q'); + } + break; + case PREDICATE_YEAR: + date = moment(moment().year() + 1, 'YYYY'); + break; + } + if (date) { + value = { + operator: '$lt', + value: date.format('YYYY-MM-DD'), + }; + } + } else if ( + operator === OPERATOR_DUE && + value === PREDICATE_OVERDUE + ) { + value = { + operator: '$lt', + value: moment().format('YYYY-MM-DD'), + }; + } else { + this.errors.addError(OPERATOR_DUE, { + tag: 'operator-number-expected', + value: { operator: op, value }, + }); + continue; + } + } else if (operator === OPERATOR_DUE) { + value = { + operator: '$lt', + value: moment(moment().format('YYYY-MM-DD')) + .add(days + 1, duration ? duration : 'days') + .format(), + }; + } else { + value = { + operator: '$gte', + value: moment(moment().format('YYYY-MM-DD')) + .subtract(days, duration ? duration : 'days') + .format(), + }; + } + } else if (operator === OPERATOR_SORT) { + let negated = false; + const m = value.match(reNegatedOperator); + if (m) { + value = m.groups.operator; + negated = true; + } + if (!predicateTranslations.sorts[value]) { + this.errors.addError(OPERATOR_SORT, { + tag: 'operator-sort-invalid', + value, + }); + continue; + } else { + value = { + name: predicateTranslations.sorts[value], + order: negated ? ORDER_DESCENDING : ORDER_ASCENDING, + }; + } + } else if (operator === OPERATOR_STATUS) { + if (!predicateTranslations.status[value]) { + this.errors.addError(OPERATOR_STATUS, { + tag: 'operator-status-invalid', + value, + }); + continue; + } else { + value = predicateTranslations.status[value]; + } + } else if (operator === OPERATOR_HAS) { + let negated = false; + const m = value.match(reNegatedOperator); + if (m) { + value = m.groups.operator; + negated = true; + } + if (!predicateTranslations.has[value]) { + this.errors.addError(OPERATOR_HAS, { + tag: 'operator-has-invalid', + value, + }); + continue; + } else { + value = { + field: predicateTranslations.has[value], + exists: !negated, + }; + } + } else if (operator === OPERATOR_LIMIT) { + const limit = parseInt(value, 10); + if (isNaN(limit) || limit < 1) { + this.errors.addError(OPERATOR_LIMIT, { + tag: 'operator-limit-invalid', + value, + }); + continue; + } else { + value = limit; + } + } + + this.queryParams.addPredicate(operator, value); + } else { + this.errors.addError(OPERATOR_UNKNOWN, { + tag: 'operator-unknown-error', + value: op, + }); + } + continue; + } + + m = queryText.match(reQuotedText); + if (!m) { + m = queryText.match(reText); + if (m) { + queryText = queryText.replace(reText, ''); + } + } else { + queryText = queryText.replace(reQuotedText, ''); + } + if (m) { + text += (text ? ' ' : '') + m.groups.text; + } + } + + // eslint-disable-next-line no-console + // console.log('text:', text); + this.queryParams.text = text; + + // eslint-disable-next-line no-console + console.log('queryParams:', this.queryParams); + } } diff --git a/server/publications/cards.js b/server/publications/cards.js index dfdf195e2..449aa28f7 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -1,7 +1,9 @@ +import moment from 'moment'; import Users from '../../models/users'; import Boards from '../../models/boards'; import Lists from '../../models/lists'; import Swimlanes from '../../models/swimlanes'; +import Cards from '../../models/cards'; import CardComments from '../../models/cardComments'; import Attachments from '../../models/attachments'; import Checklists from '../../models/checklists'; @@ -411,26 +413,26 @@ function buildSelector(queryParams) { const attachments = Attachments.find({ 'original.name': regex }); - // const comments = CardComments.find( - // { text: regex }, - // { fields: { cardId: 1 } }, - // ); + const comments = CardComments.find( + { text: regex }, + { fields: { cardId: 1 } }, + ); selector.$and.push({ $or: [ { title: regex }, { description: regex }, { customFields: { $elemMatch: { value: regex } } }, - { - _id: { - $in: CardComments.textSearch(userId, [queryParams.text]).map( - com => com.cardId, - ), - }, - }, + // { + // _id: { + // $in: CardComments.textSearch(userId, [queryParams.text]).map( + // com => com.cardId, + // ), + // }, + // }, { _id: { $in: checklists.map(list => list.cardId) } }, { _id: { $in: attachments.map(attach => attach.cardId) } }, - // { _id: { $in: comments.map(com => com.cardId) } }, + { _id: { $in: comments.map(com => com.cardId) } }, ], }); } From 183404a5f873a8afbe3e9df86238447d4deb63aa Mon Sep 17 00:00:00 2001 From: "John R. Supplee" <john@supplee.com> Date: Wed, 10 Mar 2021 12:40:20 +0200 Subject: [PATCH 12/17] Code cleanup and remove debugging code --- client/components/main/globalSearch.jade | 21 --------------------- server/publications/cards.js | 14 ++++---------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 47fba719d..77a976ee8 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -82,24 +82,3 @@ template(name="globalSearch") .global-search-instructions +viewer = searchInstructions - -template(name="globalSearchViewChangePopup") - if currentUser - ul.pop-over-list - li - with "globalSearchViewChange-choice-me" - a.js-global-search-view-me - i.fa.fa-user.colorful - | {{_ 'globalSearchViewChange-choice-me'}} - if $eq Utils.globalSearchView "me" - i.fa.fa-check - li - with "globalSearchViewChange-choice-all" - a.js-global-search-view-all - i.fa.fa-users.colorful - | {{_ 'globalSearchViewChange-choice-all'}} - span.sub-name - +viewer - | {{_ 'globalSearchViewChange-choice-all-description' }} - if $eq Utils.globalSearchView "all" - i.fa.fa-check diff --git a/server/publications/cards.js b/server/publications/cards.js index 449aa28f7..f13343c61 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -85,7 +85,7 @@ Meteor.publish('globalSearch', function(sessionId, params) { check(params, Object); // eslint-disable-next-line no-console - console.log('queryParams:', params); + // console.log('queryParams:', params); return findCards(sessionId, buildQuery(new QueryParams(params))); }); @@ -564,7 +564,7 @@ Meteor.publish('brokenCards', function(sessionId) { { swimlaneId: { $in: [null, ''] } }, { listId: { $in: [null, ''] } }, ]; - console.log('brokenCards selector:', query.selector); + // console.log('brokenCards selector:', query.selector); return findCards(sessionId, query); }); @@ -593,8 +593,8 @@ function findCards(sessionId, query) { const userId = Meteor.userId(); // eslint-disable-next-line no-console - console.log('selector:', query.selector); - console.log('selector.$and:', query.selector.$and); + // console.log('selector:', query.selector); + // console.log('selector.$and:', query.selector.$and); // eslint-disable-next-line no-console // console.log('projection:', projection); let cards; @@ -604,7 +604,6 @@ function findCards(sessionId, query) { // eslint-disable-next-line no-console // console.log('count:', cards.count()); - console.log(query); const update = { $set: { totalHits: 0, @@ -616,11 +615,6 @@ function findCards(sessionId, query) { errors: query.errors(), }, }; - // if (errors) { - // update.$set.errors = errors.errors(); - // } - - console.log('errors:', query.errors()); if (cards) { update.$set.totalHits = cards.count(); From 07aa20a3cdb7034a181d76df51abfca9d2656dee Mon Sep 17 00:00:00 2001 From: "John R. Supplee" <john@supplee.com> Date: Thu, 11 Mar 2021 02:05:46 +0200 Subject: [PATCH 13/17] Fix for Due Cards and some cleanup --- client/components/main/dueCards.js | 28 ++++++++++++++++++++-------- client/components/main/myCards.js | 16 +++++++++++++--- client/lib/cardSearch.js | 3 +++ config/query-classes.js | 4 ++++ 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/client/components/main/dueCards.js b/client/components/main/dueCards.js index 7c329586f..f08e306a0 100644 --- a/client/components/main/dueCards.js +++ b/client/components/main/dueCards.js @@ -1,4 +1,12 @@ import { CardSearchPagedComponent } from '../../lib/cardSearch'; +import { + OPERATOR_HAS, + OPERATOR_SORT, + OPERATOR_USER, + ORDER_DESCENDING, + PREDICATE_DUE_AT, +} from '../../../config/search-const'; +import { QueryParams } from '../../../config/query-classes'; // const subManager = new SubsManager(); @@ -46,18 +54,22 @@ class DueCardsComponent extends CardSearchPagedComponent { onCreated() { super.onCreated(); - const queryParams = { - has: [{ field: 'dueAt', exists: true }], - limit: 5, - skip: 0, - sort: { name: 'dueAt', order: 'des' }, - }; + const queryParams = new QueryParams(); + queryParams.addPredicate(OPERATOR_HAS, { + field: PREDICATE_DUE_AT, + exists: true, + }); + // queryParams[OPERATOR_LIMIT] = 5; + queryParams.addPredicate(OPERATOR_SORT, { + name: PREDICATE_DUE_AT, + order: ORDER_DESCENDING, + }); if (Utils.dueCardsView() !== 'all') { - queryParams.users = [Meteor.user().username]; + queryParams.addPredicate(OPERATOR_USER, Meteor.user().username); } - this.runGlobalSearch(queryParams); + this.runGlobalSearch(queryParams.getParams()); } dueCardsView() { diff --git a/client/components/main/myCards.js b/client/components/main/myCards.js index 0a2794e5b..3f79e0f26 100644 --- a/client/components/main/myCards.js +++ b/client/components/main/myCards.js @@ -1,6 +1,12 @@ import { CardSearchPagedComponent } from '../../lib/cardSearch'; -import {QueryParams} from "../../../config/query-classes"; -import {OPERATOR_SORT, OPERATOR_USER} from "../../../config/search-const"; +import { QueryParams } from '../../../config/query-classes'; +import { + OPERATOR_LIMIT, + OPERATOR_SORT, + OPERATOR_USER, + ORDER_DESCENDING, + PREDICATE_DUE_AT, +} from '../../../config/search-const'; // const subManager = new SubsManager(); @@ -52,7 +58,11 @@ class MyCardsComponent extends CardSearchPagedComponent { const queryParams = new QueryParams(); queryParams.addPredicate(OPERATOR_USER, Meteor.user().username); - queryParams.addPredicate(OPERATOR_SORT, { name: 'dueAt', order: 'des' }); + queryParams.addPredicate(OPERATOR_SORT, { + name: PREDICATE_DUE_AT, + order: ORDER_DESCENDING, + }); + queryParams.addPredicate(OPERATOR_LIMIT, 100); this.runGlobalSearch(queryParams); Meteor.subscribe('setting'); diff --git a/client/lib/cardSearch.js b/client/lib/cardSearch.js index 5af6a974f..621b8bd3d 100644 --- a/client/lib/cardSearch.js +++ b/client/lib/cardSearch.js @@ -1,3 +1,6 @@ +import Cards from '../../models/cards'; +import SessionData from '../../models/usersessiondata'; + export class CardSearchPagedComponent extends BlazeComponent { onCreated() { this.searching = new ReactiveVar(false); diff --git a/config/query-classes.js b/config/query-classes.js index aa97f306a..3120dce71 100644 --- a/config/query-classes.js +++ b/config/query-classes.js @@ -193,6 +193,10 @@ export class Query { return this.queryParams.getParams(); } + addPredicate(operator, predicate) { + this.queryParams.addPredicate(operator, predicate); + } + buildParams(queryText) { queryText = queryText.trim(); // eslint-disable-next-line no-console From 0ebb427a0044f8857da4ceae0ced2e3a2f02af93 Mon Sep 17 00:00:00 2001 From: John Supplee <john@supplee.com> Date: Thu, 18 Mar 2021 02:04:25 +0200 Subject: [PATCH 14/17] Update the server error message --- 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 9f3489df8..1bd8fdf01 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -980,7 +980,7 @@ "cardsSortPopup-title": "Sort Cards", "due-date": "Due Date", "server-error": "Server Error", - "server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation on Linux, run: `sudo journalctl -u 'snap.wekan.*'`", + "server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation, run: `sudo snap logs wekan.wekan`\nFor a Docker installation, run: `sudo docker logs wekan-app`", "title-alphabetically": "Title (Alphabetically)", "created-at-newest-first": "Created At (Newest First)", "created-at-oldest-first": "Created At (Oldest First)" From 9e8399612b9a749f199d3366e6214ac129d019bb Mon Sep 17 00:00:00 2001 From: John Supplee <john@supplee.com> Date: Fri, 19 Mar 2021 21:09:39 +0200 Subject: [PATCH 15/17] Add environment variable and snap configuration option for results per page --- server/publications/cards.js | 5 +++++ snap-src/bin/config | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/server/publications/cards.js b/server/publications/cards.js index f13343c61..99cc596a8 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -461,6 +461,11 @@ function buildProjection(query) { skip = query.params.skip; } let limit = DEFAULT_LIMIT; + const configLimit = parseInt(process.env.RESULTS_PER_PAGE, 10); + if (!isNaN(configLimit) && configLimit > 0) { + limit = configLimit; + } + if (query.params.hasOperator(OPERATOR_LIMIT)) { limit = query.params.getPredicate(OPERATOR_LIMIT); } diff --git a/snap-src/bin/config b/snap-src/bin/config index 5b426c0a8..ca5ee779a 100755 --- a/snap-src/bin/config +++ b/snap-src/bin/config @@ -3,7 +3,7 @@ # All supported keys are defined here together with descriptions and default values # list of supported keys -keys="DEBUG MONGO_LOG_DESTINATION MONGO_URL MONGODB_BIND_UNIX_SOCKET MONGO_URL MONGODB_BIND_IP MONGODB_PORT MAIL_URL MAIL_FROM ROOT_URL PORT DISABLE_MONGODB CADDY_ENABLED CADDY_BIND_PORT WITH_API RICHER_CARD_COMMENT_EDITOR CARD_OPENED_WEBHOOK_ENABLED ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW MAX_IMAGE_PIXEL IMAGE_COMPRESS_RATIO BIGEVENTS_PATTERN NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE NOTIFY_DUE_DAYS_BEFORE_AND_AFTER NOTIFY_DUE_AT_HOUR_OF_DAY EMAIL_NOTIFICATION_TIMEOUT CORS CORS_ALLOW_HEADERS CORS_EXPOSE_HEADERS MATOMO_ADDRESS MATOMO_SITE_ID MATOMO_DO_NOT_TRACK MATOMO_WITH_USERNAME BROWSER_POLICY_ENABLED TRUSTED_URL WEBHOOKS_ATTRIBUTES OAUTH2_ENABLED OAUTH2_CA_CERT OAUTH2_LOGIN_STYLE OAUTH2_CLIENT_ID OAUTH2_SECRET OAUTH2_SERVER_URL OAUTH2_AUTH_ENDPOINT OAUTH2_USERINFO_ENDPOINT OAUTH2_TOKEN_ENDPOINT OAUTH2_ID_MAP OAUTH2_USERNAME_MAP OAUTH2_FULLNAME_MAP OAUTH2_ID_TOKEN_WHITELIST_FIELDS OAUTH2_EMAIL_MAP OAUTH2_REQUEST_PERMISSIONS OAUTH2_ADFS_ENABLED LDAP_ENABLE LDAP_PORT LDAP_HOST LDAP_BASEDN LDAP_LOGIN_FALLBACK LDAP_RECONNECT LDAP_TIMEOUT LDAP_IDLE_TIMEOUT LDAP_CONNECT_TIMEOUT LDAP_AUTHENTIFICATION LDAP_AUTHENTIFICATION_USERDN LDAP_AUTHENTIFICATION_PASSWORD LDAP_LOG_ENABLED LDAP_BACKGROUND_SYNC LDAP_BACKGROUND_SYNC_INTERVAL LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS LDAP_ENCRYPTION LDAP_CA_CERT LDAP_REJECT_UNAUTHORIZED LDAP_USER_AUTHENTICATION LDAP_USER_AUTHENTICATION_FIELD LDAP_USER_SEARCH_FILTER LDAP_USER_SEARCH_SCOPE LDAP_USER_SEARCH_FIELD LDAP_SEARCH_PAGE_SIZE LDAP_SEARCH_SIZE_LIMIT LDAP_GROUP_FILTER_ENABLE LDAP_GROUP_FILTER_OBJECTCLASS LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT LDAP_GROUP_FILTER_GROUP_NAME LDAP_UNIQUE_IDENTIFIER_FIELD LDAP_UTF8_NAMES_SLUGIFY LDAP_USERNAME_FIELD LDAP_FULLNAME_FIELD LDAP_MERGE_EXISTING_USERS LDAP_SYNC_USER_DATA LDAP_SYNC_USER_DATA_FIELDMAP LDAP_SYNC_GROUP_ROLES LDAP_DEFAULT_DOMAIN LDAP_EMAIL_MATCH_ENABLE LDAP_EMAIL_MATCH_REQUIRE LDAP_EMAIL_MATCH_VERIFIED LDAP_EMAIL_FIELD LDAP_SYNC_ADMIN_STATUS LDAP_SYNC_ADMIN_GROUPS HEADER_LOGIN_ID HEADER_LOGIN_FIRSTNAME HEADER_LOGIN_LASTNAME HEADER_LOGIN_EMAIL LOGOUT_WITH_TIMER LOGOUT_IN LOGOUT_ON_HOURS LOGOUT_ON_MINUTES DEFAULT_AUTHENTICATION_METHOD ATTACHMENTS_STORE_PATH PASSWORD_LOGIN_ENABLED CAS_ENABLED CAS_BASE_URL CAS_LOGIN_URL CAS_VALIDATE_URL SAML_ENABLED SAML_PROVIDER SAML_ENTRYPOINT SAML_ISSUER SAML_CERT SAML_IDPSLO_REDIRECTURL SAML_PRIVATE_KEYFILE SAML_PUBLIC_CERTFILE SAML_IDENTIFIER_FORMAT SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE SAML_ATTRIBUTES ORACLE_OIM_ENABLED" +keys="DEBUG MONGO_LOG_DESTINATION MONGO_URL MONGODB_BIND_UNIX_SOCKET MONGO_URL MONGODB_BIND_IP MONGODB_PORT MAIL_URL MAIL_FROM ROOT_URL PORT DISABLE_MONGODB CADDY_ENABLED CADDY_BIND_PORT WITH_API RICHER_CARD_COMMENT_EDITOR CARD_OPENED_WEBHOOK_ENABLED ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW MAX_IMAGE_PIXEL IMAGE_COMPRESS_RATIO BIGEVENTS_PATTERN NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE NOTIFY_DUE_DAYS_BEFORE_AND_AFTER NOTIFY_DUE_AT_HOUR_OF_DAY EMAIL_NOTIFICATION_TIMEOUT CORS CORS_ALLOW_HEADERS CORS_EXPOSE_HEADERS MATOMO_ADDRESS MATOMO_SITE_ID MATOMO_DO_NOT_TRACK MATOMO_WITH_USERNAME BROWSER_POLICY_ENABLED TRUSTED_URL WEBHOOKS_ATTRIBUTES OAUTH2_ENABLED OAUTH2_CA_CERT OAUTH2_LOGIN_STYLE OAUTH2_CLIENT_ID OAUTH2_SECRET OAUTH2_SERVER_URL OAUTH2_AUTH_ENDPOINT OAUTH2_USERINFO_ENDPOINT OAUTH2_TOKEN_ENDPOINT OAUTH2_ID_MAP OAUTH2_USERNAME_MAP OAUTH2_FULLNAME_MAP OAUTH2_ID_TOKEN_WHITELIST_FIELDS OAUTH2_EMAIL_MAP OAUTH2_REQUEST_PERMISSIONS OAUTH2_ADFS_ENABLED LDAP_ENABLE LDAP_PORT LDAP_HOST LDAP_BASEDN LDAP_LOGIN_FALLBACK LDAP_RECONNECT LDAP_TIMEOUT LDAP_IDLE_TIMEOUT LDAP_CONNECT_TIMEOUT LDAP_AUTHENTIFICATION LDAP_AUTHENTIFICATION_USERDN LDAP_AUTHENTIFICATION_PASSWORD LDAP_LOG_ENABLED LDAP_BACKGROUND_SYNC LDAP_BACKGROUND_SYNC_INTERVAL LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS LDAP_ENCRYPTION LDAP_CA_CERT LDAP_REJECT_UNAUTHORIZED LDAP_USER_AUTHENTICATION LDAP_USER_AUTHENTICATION_FIELD LDAP_USER_SEARCH_FILTER LDAP_USER_SEARCH_SCOPE LDAP_USER_SEARCH_FIELD LDAP_SEARCH_PAGE_SIZE LDAP_SEARCH_SIZE_LIMIT LDAP_GROUP_FILTER_ENABLE LDAP_GROUP_FILTER_OBJECTCLASS LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT LDAP_GROUP_FILTER_GROUP_NAME LDAP_UNIQUE_IDENTIFIER_FIELD LDAP_UTF8_NAMES_SLUGIFY LDAP_USERNAME_FIELD LDAP_FULLNAME_FIELD LDAP_MERGE_EXISTING_USERS LDAP_SYNC_USER_DATA LDAP_SYNC_USER_DATA_FIELDMAP LDAP_SYNC_GROUP_ROLES LDAP_DEFAULT_DOMAIN LDAP_EMAIL_MATCH_ENABLE LDAP_EMAIL_MATCH_REQUIRE LDAP_EMAIL_MATCH_VERIFIED LDAP_EMAIL_FIELD LDAP_SYNC_ADMIN_STATUS LDAP_SYNC_ADMIN_GROUPS HEADER_LOGIN_ID HEADER_LOGIN_FIRSTNAME HEADER_LOGIN_LASTNAME HEADER_LOGIN_EMAIL LOGOUT_WITH_TIMER LOGOUT_IN LOGOUT_ON_HOURS LOGOUT_ON_MINUTES DEFAULT_AUTHENTICATION_METHOD ATTACHMENTS_STORE_PATH PASSWORD_LOGIN_ENABLED CAS_ENABLED CAS_BASE_URL CAS_LOGIN_URL CAS_VALIDATE_URL SAML_ENABLED SAML_PROVIDER SAML_ENTRYPOINT SAML_ISSUER SAML_CERT SAML_IDPSLO_REDIRECTURL SAML_PRIVATE_KEYFILE SAML_PUBLIC_CERTFILE SAML_IDENTIFIER_FORMAT SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE SAML_ATTRIBUTES ORACLE_OIM_ENABLED RESULTS_PER_PAGE" # default values DESCRIPTION_DEBUG="Debug OIDC OAuth2 etc. Example: sudo snap set wekan debug='true'" @@ -543,3 +543,7 @@ KEY_SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE="saml-local-profile-match-attribute" DESCRIPTION_SAML_ATTRIBUTES="SAML Attributes" DEFAULT_SAML_ATTRIBUTES="" KEY_SAML_ATTRIBUTES="saml-attributes" + +DESCRIPTION_RESULTS_PER_PAGE="Number of results to show per page by default" +DEFAULT_RESULTS_PER_PAGE="" +KEY_RESULTS_PER_PAGE="results-per-page" From 39b4ada26daf040854d4064795e96664de2e1b5c Mon Sep 17 00:00:00 2001 From: "John R. Supplee" <john@supplee.com> Date: Sun, 28 Mar 2021 02:45:15 +0200 Subject: [PATCH 16/17] Do not include archived lists in global search help --- models/lists.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/models/lists.js b/models/lists.js index 141804492..0bf8085fe 100644 --- a/models/lists.js +++ b/models/lists.js @@ -378,8 +378,13 @@ Meteor.methods({ // my lists return _.uniq( Lists.find( - { boardId: { $in: Boards.userBoardIds(this.userId) } }, - { fields: { title: 1 } }, + { + boardId: { $in: Boards.userBoardIds(this.userId) }, + archived: false, + }, + { + fields: { title: 1 }, + }, ) .fetch() .map(list => { From 20a2ea3e7ab325a3a05e0ae09a475e2bdece2269 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" <john@supplee.com> Date: Sun, 28 Mar 2021 02:46:11 +0200 Subject: [PATCH 17/17] Add icon to clear the current search --- client/components/main/globalSearch.jade | 2 ++ client/components/main/globalSearch.js | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 77a976ee8..bd2493124 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -32,6 +32,7 @@ template(name="globalSearch") .wrapper form.global-search-page.js-search-query-form input.global-search-query-input( + style="{# if hasResults.get #}display: inline-block;{#/if#}" id="global-search-input" type="text" name="searchQuery" @@ -39,6 +40,7 @@ template(name="globalSearch") value="{{ query.get }}" autofocus dir="auto" ) + a.js-new-search.fa.fa-eraser if searching.get +spinner else if hasResults.get diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 294b3dace..133fa1433 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -249,6 +249,13 @@ class GlobalSearchComponent extends CardSearchPagedComponent { ); document.getElementById('global-search-input').focus(); }, + 'click .js-new-search'(evt) { + evt.preventDefault(); + const input = document.getElementById('global-search-input'); + input.value = ''; + this.query.set(''); + this.hasResults.set(false); + }, }, ]; }