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..17d30f37c 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/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..f08e306a0 100644 --- a/client/components/main/dueCards.js +++ b/client/components/main/dueCards.js @@ -1,4 +1,14 @@ -const subManager = new SubsManager(); +import { CardSearchPagedComponent } from '../../lib/cardSearch'; +import { + OPERATOR_HAS, + OPERATOR_SORT, + OPERATOR_USER, + ORDER_DESCENDING, + PREDICATE_DUE_AT, +} from '../../../config/search-const'; +import { QueryParams } from '../../../config/query-classes'; + +// const subManager = new SubsManager(); BlazeComponent.extendComponent({ dueCardsView() { @@ -40,106 +50,51 @@ 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()); - }); - }); + const queryParams = new QueryParams(); + queryParams.addPredicate(OPERATOR_HAS, { + field: PREDICATE_DUE_AT, + exists: true, }); - Meteor.subscribe('setting'); - }, + // queryParams[OPERATOR_LIMIT] = 5; + queryParams.addPredicate(OPERATOR_SORT, { + name: PREDICATE_DUE_AT, + order: ORDER_DESCENDING, + }); + + if (Utils.dueCardsView() !== 'all') { + queryParams.addPredicate(OPERATOR_USER, Meteor.user().username); + } + + this.runGlobalSearch(queryParams.getParams()); + } dueCardsView() { // 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 = []; + if (results) { + results.forEach(card => { + cards.push(card); + }); } - 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(), - // ); - }); - 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; @@ -148,7 +103,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..bd2493124 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -10,11 +10,29 @@ 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 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" @@ -22,31 +40,24 @@ template(name="globalSearch") value="{{ query.get }}" autofocus dir="auto" ) + a.js-new-search.fa.fa-eraser if searching.get +spinner 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 - h1 - = resultsHeading.get - a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}") - each card in results.get - +resultCard(card) - table.global-search-footer - tr - td.global-search-previous-page - if hasPreviousPage.get - button.js-previous-page - | {{_ 'previous-page' }} - td.global-search-next-page(align="right") - if hasNextPage.get - button.js-next-page - | {{_ 'next-page' }} + +resultsPaged(this) + else if serverError.get + .global-search-page + .global-search-help + h1 {{_ 'server-error' }} + +viewer + | {{_ 'server-error-troubleshooting' }} else .global-search-page .global-search-help @@ -73,24 +84,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/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 56acb47a7..133fa1433 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -1,4 +1,8 @@ -const subManager = new SubsManager(); +import { CardSearchPagedComponent } from '../../lib/cardSearch'; +import Boards from '../../../models/boards'; +import { Query, QueryErrors } from '../../../config/query-classes'; + +// const subManager = new SubsManager(); BlazeComponent.extendComponent({ events() { @@ -16,45 +20,14 @@ Template.globalSearch.helpers({ }, }); -BlazeComponent.extendComponent({ - events() { - return [ - { - 'click .js-due-cards-view-me'() { - Utils.setDueCardsView('me'); - Popup.close(); - }, - - 'click .js-due-cards-view-all'() { - Utils.setDueCardsView('all'); - Popup.close(); - }, - }, - ]; - }, -}).register('globalSearchViewChangePopup'); - -BlazeComponent.extendComponent({ +class GlobalSearchComponent extends CardSearchPagedComponent { onCreated() { - 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.parsingErrors = new QueryErrors(); this.queryParams = null; - this.parsingErrors = []; - this.resultsCount = 0; - this.totalHits = 0; - this.queryErrors = null; - this.colorMap = null; - this.resultsPerPage = 25; Meteor.call('myLists', (err, data) => { if (!err) { @@ -73,510 +46,71 @@ BlazeComponent.extendComponent({ this.myBoardNames.set(data); } }); - }, + } onRendered() { Meteor.subscribe('setting'); // 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')); } - }, + } resetSearch() { - this.searching.set(false); - this.results.set([]); - this.hasResults.set(false); - this.hasQueryErrors.set(false); - this.resultsHeading.set(''); - this.parsingErrors = []; - this.resultsCount = 0; - this.totalHits = 0; - this.queryErrors = null; - }, - - getSessionData() { - return SessionData.findOne({ - userId: Meteor.userId(), - sessionId: SessionData.getSessionId(), - }); - }, - - getResults() { - // eslint-disable-next-line no-console - // console.log('getting results'); - if (this.queryParams) { - const sessionData = this.getSessionData(); - // eslint-disable-next-line no-console - // console.log('selector:', sessionData.getSelector()); - // console.log('session data:', sessionData); - const projection = sessionData.getProjection(); - projection.skip = 0; - const cards = Cards.find({ _id: { $in: sessionData.cards } }, projection); - this.queryErrors = sessionData.errors; - if (this.queryErrors.length) { - this.hasQueryErrors.set(true); - return null; - } - - if (cards) { - this.totalHits = sessionData.totalHits; - this.resultsCount = cards.count(); - this.resultsStart = sessionData.lastHit - this.resultsCount + 1; - this.resultsEnd = sessionData.lastHit; - this.resultsHeading.set(this.getResultsHeading()); - this.results.set(cards); - this.hasNextPage.set(sessionData.lastHit < sessionData.totalHits); - this.hasPreviousPage.set( - sessionData.lastHit - sessionData.resultsCount > 0, - ); - } - } - this.resultsCount = 0; - return null; - }, + super.resetSearch(); + this.parsingErrors = new QueryErrors(); + } errorMessages() { - if (this.parsingErrors.length) { - return this.parsingErrorMessages(); + if (this.parsingErrors.hasErrors()) { + return this.parsingErrors.errorMessages(); } return this.queryErrorMessages(); - }, + } parsingErrorMessages() { - const messages = []; + this.parsingErrors.errorMessages(); + } - if (this.parsingErrors.length) { - this.parsingErrors.forEach(err => { - messages.push(TAPi18n.__(err.tag, err.value)); - }); - } - - return messages; - }, - - queryErrorMessages() { - messages = []; - - this.queryErrors.forEach(err => { - let value = err.color ? TAPi18n.__(`color-${err.value}`) : err.value; - if (!value) { - value = err.value; - } - messages.push(TAPi18n.__(err.tag, value)); - }); - - return messages; - }, - - searchAllBoards(query) { - query = query.trim(); + searchAllBoards(queryText) { + queryText = queryText.trim(); // eslint-disable-next-line no-console - //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( - '^((?[\\p{Letter}\\p{Mark}]+):|(?[#@]))(?[\\p{Letter}\\p{Mark}]+)(\\s+|$)', - 'iu', - ); - const reOperator2 = new RegExp( - '^((?[\\p{Letter}\\p{Mark}]+):|(?[#@]))(?["\']*)(?.*?)\\k(\\s+|$)', - 'iu', - ); - const reText = new RegExp('^(?\\S+)(\\s+|$)', 'u'); - const reQuotedText = new RegExp( - '^(?["\'])(?.*?)\\k(\\s+|$)', - 'u', - ); - const reNegatedOperator = new RegExp('^-(?.*)$'); - - const operators = { - 'operator-board': 'boards', - 'operator-board-abbrev': 'boards', - 'operator-swimlane': 'swimlanes', - 'operator-swimlane-abbrev': 'swimlanes', - 'operator-list': 'lists', - 'operator-list-abbrev': 'lists', - 'operator-label': 'labels', - 'operator-label-abbrev': 'labels', - 'operator-user': 'users', - 'operator-user-abbrev': 'users', - 'operator-member': 'members', - 'operator-member-abbrev': 'members', - 'operator-assignee': 'assignees', - 'operator-assignee-abbrev': 'assignees', - 'operator-status': 'status', - 'operator-due': 'dueAt', - 'operator-created': 'createdAt', - 'operator-modified': 'modifiedAt', - 'operator-comment': 'comments', - 'operator-has': 'has', - 'operator-sort': 'sort', - 'operator-limit': 'limit', - }; - - const predicates = { - due: { - 'predicate-overdue': 'overdue', - }, - durations: { - 'predicate-week': 'week', - 'predicate-month': 'month', - 'predicate-quarter': 'quarter', - 'predicate-year': 'year', - }, - status: { - 'predicate-archived': 'archived', - 'predicate-all': 'all', - 'predicate-open': 'open', - 'predicate-ended': 'ended', - 'predicate-public': 'public', - 'predicate-private': 'private', - }, - sorts: { - 'predicate-due': 'dueAt', - 'predicate-created': 'createdAt', - 'predicate-modified': 'modifiedAt', - }, - has: { - 'predicate-description': 'description', - 'predicate-checklist': 'checklist', - 'predicate-attachment': 'attachment', - 'predicate-start': 'startAt', - 'predicate-end': 'endAt', - 'predicate-due': 'dueAt', - 'predicate-assignee': 'assignees', - 'predicate-member': 'members', - }, - }; - const predicateTranslations = {}; - Object.entries(predicates).forEach(([category, catPreds]) => { - predicateTranslations[category] = {}; - Object.entries(catPreds).forEach(([tag, value]) => { - predicateTranslations[category][TAPi18n.__(tag)] = value; - }); - }); - // eslint-disable-next-line no-console - // console.log('predicateTranslations:', predicateTranslations); - - const operatorMap = {}; - Object.entries(operators).forEach(([key, value]) => { - operatorMap[TAPi18n.__(key).toLowerCase()] = value; - }); - // eslint-disable-next-line no-console - // console.log('operatorMap:', operatorMap); - - const params = { - limit: this.resultsPerPage, - boards: [], - swimlanes: [], - lists: [], - users: [], - members: [], - assignees: [], - labels: [], - status: [], - dueAt: null, - createdAt: null, - modifiedAt: null, - comments: [], - has: [], - }; - - let text = ''; - while (query) { - m = query.match(reOperator1); - if (!m) { - m = query.match(reOperator2); - if (m) { - query = query.replace(reOperator2, ''); - } - } else { - query = query.replace(reOperator1, ''); - } - if (m) { - let op; - if (m.groups.operator) { - op = m.groups.operator.toLowerCase(); - } else { - op = m.groups.abbrev.toLowerCase(); - } - // eslint-disable-next-line no-prototype-builtins - if (operatorMap.hasOwnProperty(op)) { - const operator = operatorMap[op]; - let value = m.groups.value; - if (operator === 'labels') { - if (value in this.colorMap) { - value = this.colorMap[value]; - // console.log('found color:', value); - } - } else if (['dueAt', 'createdAt', 'modifiedAt'].includes(operator)) { - let days = parseInt(value, 10); - let duration = null; - if (isNaN(days)) { - // duration was specified as text - if (predicateTranslations.durations[value]) { - duration = predicateTranslations.durations[value]; - let date = null; - switch (duration) { - case 'week': - let week = moment().week(); - if (week === 52) { - date = moment(1, 'W'); - date.set('year', date.year() + 1); - } else { - date = moment(week + 1, 'W'); - } - break; - case 'month': - let month = moment().month(); - // .month() is zero indexed - if (month === 11) { - date = moment(1, 'M'); - date.set('year', date.year() + 1); - } else { - date = moment(month + 2, 'M'); - } - break; - case 'quarter': - let quarter = moment().quarter(); - if (quarter === 4) { - date = moment(1, 'Q'); - date.set('year', date.year() + 1); - } else { - date = moment(quarter + 1, 'Q'); - } - break; - case 'year': - date = moment(moment().year() + 1, 'YYYY'); - break; - } - if (date) { - value = { - operator: '$lt', - value: date.format('YYYY-MM-DD'), - }; - } - } else if (operator === 'dueAt' && value === 'overdue') { - value = { - operator: '$lt', - value: moment().format('YYYY-MM-DD'), - }; - } else { - this.parsingErrors.push({ - tag: 'operator-number-expected', - value: { operator: op, value }, - }); - value = null; - } - } else { - if (operator === 'dueAt') { - value = { - operator: '$lt', - value: moment(moment().format('YYYY-MM-DD')) - .add(days + 1, duration ? duration : 'days') - .format(), - }; - } else { - value = { - operator: '$gte', - value: moment(moment().format('YYYY-MM-DD')) - .subtract(days, duration ? duration : 'days') - .format(), - }; - } - } - } else if (operator === 'sort') { - let negated = false; - const m = value.match(reNegatedOperator); - if (m) { - value = m.groups.operator; - negated = true; - } - if (!predicateTranslations.sorts[value]) { - this.parsingErrors.push({ - tag: 'operator-sort-invalid', - value, - }); - } else { - value = { - name: predicateTranslations.sorts[value], - order: negated ? 'des' : 'asc', - }; - } - } else if (operator === 'status') { - if (!predicateTranslations.status[value]) { - this.parsingErrors.push({ - tag: 'operator-status-invalid', - value, - }); - } else { - value = predicateTranslations.status[value]; - } - } else if (operator === 'has') { - let negated = false; - const m = value.match(reNegatedOperator); - if (m) { - value = m.groups.operator; - negated = true; - } - if (!predicateTranslations.has[value]) { - this.parsingErrors.push({ - tag: 'operator-has-invalid', - value, - }); - } else { - value = { - field: predicateTranslations.has[value], - exists: !negated, - }; - } - } else if (operator === 'limit') { - const limit = parseInt(value, 10); - if (isNaN(limit) || limit < 1) { - this.parsingErrors.push({ - tag: 'operator-limit-invalid', - value, - }); - } else { - value = limit; - } - } - if (Array.isArray(params[operator])) { - params[operator].push(value); - } else { - params[operator] = value; - } - } else { - this.parsingErrors.push({ - tag: 'operator-unknown-error', - value: op, - }); - } - continue; - } - - m = query.match(reQuotedText); - if (!m) { - m = query.match(reText); - if (m) { - query = query.replace(reText, ''); - } - } else { - query = query.replace(reQuotedText, ''); - } - if (m) { - text += (text ? ' ' : '') + m.groups.text; - } - } + 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.length) { + 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.autorun(() => { - const handle = Meteor.subscribe( - 'globalSearch', - SessionData.getSessionId(), - params, - ); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - if (handle.ready()) { - this.getResults(); - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); - }, - - nextPage() { - const sessionData = this.getSessionData(); - - this.autorun(() => { - const handle = Meteor.subscribe('nextPage', sessionData.sessionId); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - if (handle.ready()) { - this.getResults(); - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); - }, - - previousPage() { - const sessionData = this.getSessionData(); - - this.autorun(() => { - const handle = Meteor.subscribe('previousPage', sessionData.sessionId); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - if (handle.ready()) { - this.getResults(); - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); - }, - - getResultsHeading() { - if (this.resultsCount === 0) { - return TAPi18n.__('no-cards-found'); - } else if (this.resultsCount === 1) { - return TAPi18n.__('one-card-found'); - } else if (this.resultsCount === this.totalHits) { - return TAPi18n.__('n-cards-found', this.resultsCount); - } - - return TAPi18n.__('n-n-of-n-cards-found', { - start: this.resultsStart, - end: this.resultsEnd, - total: this.totalHits, - }); - }, - - getSearchHref() { - const baseUrl = window.location.href.replace(/([?#].*$|\s*$)/, ''); - return `${baseUrl}?q=${encodeURIComponent(this.query.get())}`; - }, + this.runGlobalSearch(query.getParams()); + } searchInstructions() { - tags = { + const tags = { operator_board: TAPi18n.__('operator-board'), operator_list: TAPi18n.__('operator-list'), operator_swimlane: TAPi18n.__('operator-swimlane'), @@ -618,61 +152,46 @@ BlazeComponent.extendComponent({ 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; - }, + } labelColors() { return Boards.simpleSchema()._schema['labels.$.color'].allowedValues.map( @@ -680,23 +199,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'); @@ -737,7 +249,16 @@ BlazeComponent.extendComponent({ ); document.getElementById('global-search-input').focus(); }, + 'click .js-new-search'(evt) { + evt.preventDefault(); + const input = document.getElementById('global-search-input'); + input.value = ''; + this.query.set(''); + this.hasResults.set(false); + }, }, ]; - }, -}).register('globalSearch'); + } +} + +GlobalSearchComponent.register('globalSearch'); diff --git a/client/components/main/myCards.jade b/client/components/main/myCards.jade index 01a9027fb..e721c281c 100644 --- a/client/components/main/myCards.jade +++ b/client/components/main/myCards.jade @@ -24,7 +24,9 @@ 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 @@ -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..3f79e0f26 100644 --- a/client/components/main/myCards.js +++ b/client/components/main/myCards.js @@ -1,4 +1,14 @@ -const subManager = new SubsManager(); +import { CardSearchPagedComponent } from '../../lib/cardSearch'; +import { QueryParams } from '../../../config/query-classes'; +import { + OPERATOR_LIMIT, + OPERATOR_SORT, + OPERATOR_USER, + ORDER_DESCENDING, + PREDICATE_DUE_AT, +} from '../../../config/search-const'; + +// const subManager = new SubsManager(); BlazeComponent.extendComponent({ myCardsSort() { @@ -42,182 +52,147 @@ 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 = new QueryParams(); + queryParams.addPredicate(OPERATOR_USER, Meteor.user().username); + queryParams.addPredicate(OPERATOR_SORT, { + name: PREDICATE_DUE_AT, + order: ORDER_DESCENDING, }); + queryParams.addPredicate(OPERATOR_LIMIT, 100); + + this.runGlobalSearch(queryParams); Meteor.subscribe('setting'); - }, + } 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) => { - 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; @@ -228,23 +203,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/client/lib/cardSearch.js b/client/lib/cardSearch.js new file mode 100644 index 000000000..621b8bd3d --- /dev/null +++ b/client/lib/cardSearch.js @@ -0,0 +1,182 @@ +import Cards from '../../models/cards'; +import SessionData from '../../models/usersessiondata'; + +export class CardSearchPagedComponent extends BlazeComponent { + onCreated() { + this.searching = new ReactiveVar(false); + this.hasResults = new ReactiveVar(false); + this.hasQueryErrors = new ReactiveVar(false); + this.query = new ReactiveVar(''); + this.resultsHeading = new ReactiveVar(''); + this.searchLink = new ReactiveVar(null); + this.results = new ReactiveVar([]); + this.hasNextPage = new ReactiveVar(false); + this.hasPreviousPage = new ReactiveVar(false); + this.resultsCount = 0; + this.totalHits = 0; + this.queryErrors = null; + this.resultsPerPage = 25; + this.sessionId = SessionData.getSessionId(); + this.subscriptionHandle = null; + this.serverError = new ReactiveVar(false); + + const that = this; + this.subscriptionCallbacks = { + onReady() { + that.getResults(); + that.searching.set(false); + that.hasResults.set(true); + that.serverError.set(false); + }, + onError(error) { + that.searching.set(false); + that.hasResults.set(false); + that.serverError.set(true); + console.log('Error.reason:', error.reason); + console.log('Error.message:', error.message); + console.log('Error.stack:', error.stack); + }, + }; + } + + resetSearch() { + this.searching.set(false); + this.results.set([]); + this.hasResults.set(false); + this.hasQueryErrors.set(false); + this.resultsHeading.set(''); + this.serverError.set(false); + this.resultsCount = 0; + this.totalHits = 0; + this.queryErrors = null; + } + + getSessionData(sessionId) { + return SessionData.findOne({ + sessionId: sessionId ? sessionId : SessionData.getSessionId(), + }); + } + + getResults() { + // eslint-disable-next-line no-console + // console.log('getting results'); + const sessionData = this.getSessionData(); + // eslint-disable-next-line no-console + // console.log('selector:', sessionData.getSelector()); + console.log('session data:', sessionData); + const cards = []; + sessionData.cards.forEach(cardId => { + cards.push(Cards.findOne({ _id: cardId })); + }); + this.queryErrors = sessionData.errors; + if (this.queryErrors.length) { + // console.log('queryErrors:', this.queryErrorMessages()); + this.hasQueryErrors.set(true); + return null; + } + + if (cards) { + this.totalHits = sessionData.totalHits; + this.resultsCount = cards.length; + this.resultsStart = sessionData.lastHit - this.resultsCount + 1; + this.resultsEnd = sessionData.lastHit; + this.resultsHeading.set(this.getResultsHeading()); + this.results.set(cards); + this.hasNextPage.set(sessionData.lastHit < sessionData.totalHits); + this.hasPreviousPage.set( + sessionData.lastHit - sessionData.resultsCount > 0, + ); + return cards; + } + + this.resultsCount = 0; + return null; + } + + stopSubscription() { + if (this.subscriptionHandle) { + this.subscriptionHandle.stop(); + } + } + + runGlobalSearch(params) { + this.searching.set(true); + this.stopSubscription(); + this.subscriptionHandle = Meteor.subscribe( + 'globalSearch', + this.sessionId, + params, + this.subscriptionCallbacks, + ); + } + + queryErrorMessages() { + const messages = []; + + this.queryErrors.forEach(err => { + let value = err.color ? TAPi18n.__(`color-${err.value}`) : err.value; + if (!value) { + value = err.value; + } + messages.push(TAPi18n.__(err.tag, value)); + }); + + return messages; + } + + nextPage() { + this.searching.set(true); + this.stopSubscription(); + this.subscriptionHandle = Meteor.subscribe( + 'nextPage', + this.sessionId, + this.subscriptionCallbacks, + ); + } + + previousPage() { + this.searching.set(true); + this.stopSubscription(); + this.subscriptionHandle = Meteor.subscribe( + 'previousPage', + this.sessionId, + this.subscriptionCallbacks, + ); + } + + getResultsHeading() { + if (this.resultsCount === 0) { + return TAPi18n.__('no-cards-found'); + } else if (this.resultsCount === 1) { + return TAPi18n.__('one-card-found'); + } else if (this.resultsCount === this.totalHits) { + return TAPi18n.__('n-cards-found', this.resultsCount); + } + + return TAPi18n.__('n-n-of-n-cards-found', { + start: this.resultsStart, + end: this.resultsEnd, + total: this.totalHits, + }); + } + + getSearchHref() { + const baseUrl = window.location.href.replace(/([?#].*$|\s*$)/, ''); + return `${baseUrl}?q=${encodeURIComponent(this.query.get())}`; + } + + events() { + return [ + { + 'click .js-next-page'(evt) { + evt.preventDefault(); + this.nextPage(); + }, + 'click .js-previous-page'(evt) { + evt.preventDefault(); + this.previousPage(); + }, + }, + ]; + } +} diff --git a/config/query-classes.js b/config/query-classes.js new file mode 100644 index 000000000..3120dce71 --- /dev/null +++ b/config/query-classes.js @@ -0,0 +1,503 @@ +import { + OPERATOR_ASSIGNEE, + OPERATOR_BOARD, + OPERATOR_COMMENT, + OPERATOR_CREATED_AT, + OPERATOR_DUE, + OPERATOR_HAS, + OPERATOR_LABEL, + OPERATOR_LIMIT, + OPERATOR_LIST, + OPERATOR_MEMBER, + OPERATOR_MODIFIED_AT, + OPERATOR_SORT, + OPERATOR_STATUS, + OPERATOR_SWIMLANE, + OPERATOR_UNKNOWN, + OPERATOR_USER, + ORDER_ASCENDING, + ORDER_DESCENDING, + PREDICATE_ALL, + PREDICATE_ARCHIVED, + PREDICATE_ASSIGNEES, + PREDICATE_ATTACHMENT, + PREDICATE_CHECKLIST, + PREDICATE_CREATED_AT, + PREDICATE_DESCRIPTION, + PREDICATE_DUE_AT, + PREDICATE_END_AT, + PREDICATE_ENDED, + PREDICATE_MEMBERS, + PREDICATE_MODIFIED_AT, + PREDICATE_MONTH, + PREDICATE_OPEN, + PREDICATE_OVERDUE, + PREDICATE_PRIVATE, + PREDICATE_PUBLIC, + PREDICATE_QUARTER, + PREDICATE_START_AT, + PREDICATE_WEEK, + PREDICATE_YEAR, +} from './search-const'; +import Boards from '../models/boards'; +import moment from 'moment'; + +export class QueryParams { + text = ''; + + constructor(params = {}) { + this.params = params; + } + + hasOperator(operator) { + return this.params[operator]; + } + + addPredicate(operator, predicate) { + if (!this.hasOperator(operator)) { + this.params[operator] = []; + } + this.params[operator].push(predicate); + } + + setPredicate(operator, predicate) { + this.params[operator] = predicate; + } + + getPredicate(operator) { + return this.params[operator][0]; + } + + getPredicates(operator) { + return this.params[operator]; + } + + getParams() { + return this.params; + } +} + +export class QueryErrors { + operatorTagMap = [ + [OPERATOR_BOARD, 'board-title-not-found'], + [OPERATOR_SWIMLANE, 'swimlane-title-not-found'], + [ + OPERATOR_LABEL, + label => { + if (Boards.labelColors().includes(label)) { + return { + tag: 'label-color-not-found', + value: label, + color: true, + }; + } else { + return { + tag: 'label-not-found', + value: label, + color: false, + }; + } + }, + ], + [OPERATOR_LIST, 'list-title-not-found'], + [OPERATOR_COMMENT, 'comment-not-found'], + [OPERATOR_USER, 'user-username-not-found'], + [OPERATOR_ASSIGNEE, 'user-username-not-found'], + [OPERATOR_MEMBER, 'user-username-not-found'], + ]; + + constructor() { + this._errors = {}; + + this.operatorTags = {}; + this.operatorTagMap.forEach(([operator, tag]) => { + this.operatorTags[operator] = tag; + }); + + this.colorMap = Boards.colorMap(); + } + + addError(operator, error) { + if (!this._errors[operator]) { + this._errors[operator] = []; + } + this._errors[operator].push(error); + } + + addNotFound(operator, value) { + if (typeof this.operatorTags[operator] === 'function') { + this.addError(operator, this.operatorTags[operator](value)); + } else { + this.addError(operator, { tag: this.operatorTags[operator], value }); + } + } + + hasErrors() { + return Object.entries(this._errors).length > 0; + } + + errors() { + const errs = []; + // eslint-disable-next-line no-unused-vars + Object.entries(this._errors).forEach(([, errors]) => { + errors.forEach(err => { + errs.push(err); + }); + }); + return errs; + } + + errorMessages() { + const messages = []; + // eslint-disable-next-line no-unused-vars + Object.entries(this._errors).forEach(([, errors]) => { + errors.forEach(err => { + messages.push(TAPi18n.__(err.tag, err.value)); + }); + }); + return messages; + } +} + +export class Query { + selector = {}; + projection = {}; + + constructor(selector, projection) { + this._errors = new QueryErrors(); + this.queryParams = new QueryParams(); + this.colorMap = Boards.colorMap(); + + if (selector) { + this.selector = selector; + } + + if (projection) { + this.projection = projection; + } + } + + hasErrors() { + return this._errors.hasErrors(); + } + + errors() { + return this._errors.errors(); + } + + errorMessages() { + return this._errors.errorMessages(); + } + + getParams() { + return this.queryParams.getParams(); + } + + addPredicate(operator, predicate) { + this.queryParams.addPredicate(operator, predicate); + } + + buildParams(queryText) { + queryText = queryText.trim(); + // eslint-disable-next-line no-console + //console.log('query:', query); + + if (!queryText) { + return; + } + + const reOperator1 = new RegExp( + '^((?[\\p{Letter}\\p{Mark}]+):|(?[#@]))(?[\\p{Letter}\\p{Mark}]+)(\\s+|$)', + 'iu', + ); + const reOperator2 = new RegExp( + '^((?[\\p{Letter}\\p{Mark}]+):|(?[#@]))(?["\']*)(?.*?)\\k(\\s+|$)', + 'iu', + ); + const reText = new RegExp('^(?\\S+)(\\s+|$)', 'u'); + const reQuotedText = new RegExp( + '^(?["\'])(?.*?)\\k(\\s+|$)', + 'u', + ); + const reNegatedOperator = new RegExp('^-(?.*)$'); + + 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/config/search-const.js b/config/search-const.js new file mode 100644 index 000000000..26f8ad00b --- /dev/null +++ b/config/search-const.js @@ -0,0 +1,41 @@ +export const DEFAULT_LIMIT = 25; +export const OPERATOR_ASSIGNEE = 'assignee'; +export const OPERATOR_COMMENT = 'comment'; +export const OPERATOR_CREATED_AT = 'createdAt'; +export const OPERATOR_DUE = 'dueAt'; +export const OPERATOR_BOARD = 'board'; +export const OPERATOR_HAS = 'has'; +export const OPERATOR_LABEL = 'label'; +export const OPERATOR_LIMIT = 'limit'; +export const OPERATOR_LIST = 'list'; +export const OPERATOR_MEMBER = 'member'; +export const OPERATOR_MODIFIED_AT = 'modifiedAt'; +export const OPERATOR_SORT = 'sort'; +export const OPERATOR_STATUS = 'status'; +export const OPERATOR_SWIMLANE = 'swimlane'; +export const OPERATOR_UNKNOWN = 'unknown'; +export const OPERATOR_USER = 'user'; +export const ORDER_ASCENDING = 'asc'; +export const ORDER_DESCENDING = 'des'; +export const PREDICATE_ALL = 'all'; +export const PREDICATE_ARCHIVED = 'archived'; +export const PREDICATE_ASSIGNEES = 'assignees'; +export const PREDICATE_ATTACHMENT = 'attachment'; +export const PREDICATE_CHECKLIST = 'checklist'; +export const PREDICATE_CREATED_AT = 'createdAt'; +export const PREDICATE_DESCRIPTION = 'description'; +export const PREDICATE_DUE_AT = 'dueAt'; +export const PREDICATE_END_AT = 'endAt'; +export const PREDICATE_ENDED = 'ended'; +export const PREDICATE_MEMBERS = 'members'; +export const PREDICATE_MODIFIED_AT = 'modifiedAt'; +export const PREDICATE_MONTH = 'month'; +export const PREDICATE_OPEN = 'open'; +export const PREDICATE_OVERDUE = 'overdue'; +export const PREDICATE_PRIVATE = 'private'; +export const PREDICATE_PUBLIC = 'public'; +export const PREDICATE_QUARTER = 'quarter'; +export const PREDICATE_START_AT = 'startAt'; +export const PREDICATE_SYSTEM = 'system'; +export const PREDICATE_WEEK = 'week'; +export const PREDICATE_YEAR = 'year'; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 751d08aaa..e20645c51 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -947,7 +947,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*", @@ -979,6 +979,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, 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)", diff --git a/models/lists.js b/models/lists.js index 68728897b..49cda140b 100644 --- a/models/lists.js +++ b/models/lists.js @@ -407,8 +407,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 => { diff --git a/server/publications/cards.js b/server/publications/cards.js index 53f831557..99cc596a8 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -1,3 +1,50 @@ +import moment from 'moment'; +import Users from '../../models/users'; +import Boards from '../../models/boards'; +import Lists from '../../models/lists'; +import Swimlanes from '../../models/swimlanes'; +import Cards from '../../models/cards'; +import CardComments from '../../models/cardComments'; +import Attachments from '../../models/attachments'; +import Checklists from '../../models/checklists'; +import ChecklistItems from '../../models/checklistItems'; +import SessionData from '../../models/usersessiondata'; +import CustomFields from '../../models/customFields'; +import { + DEFAULT_LIMIT, + OPERATOR_ASSIGNEE, + OPERATOR_BOARD, + OPERATOR_COMMENT, + OPERATOR_DUE, + OPERATOR_HAS, + OPERATOR_LABEL, + OPERATOR_LIMIT, + OPERATOR_LIST, + OPERATOR_MEMBER, + OPERATOR_SORT, + OPERATOR_STATUS, + OPERATOR_SWIMLANE, + OPERATOR_USER, + ORDER_ASCENDING, + PREDICATE_ALL, + PREDICATE_ARCHIVED, + PREDICATE_ASSIGNEES, + PREDICATE_ATTACHMENT, + PREDICATE_CHECKLIST, + PREDICATE_CREATED_AT, + PREDICATE_DESCRIPTION, + PREDICATE_DUE_AT, + PREDICATE_END_AT, + PREDICATE_ENDED, + PREDICATE_MEMBERS, + PREDICATE_MODIFIED_AT, + PREDICATE_PRIVATE, + PREDICATE_PUBLIC, + PREDICATE_START_AT, + PREDICATE_SYSTEM, +} from '../../config/search-const'; +import { QueryErrors, QueryParams, Query } from '../../config/query-classes'; + const escapeForRegex = require('escape-string-regexp'); Meteor.publish('card', cardId => { @@ -5,260 +52,50 @@ Meteor.publish('card', cardId => { return Cards.find({ _id: cardId }); }); -Meteor.publish('myCards', function() { - const userId = Meteor.userId(); +Meteor.publish('myCards', function(sessionId) { + const queryParams = new QueryParams(); + queryParams.addPredicate(OPERATOR_USER, Meteor.user().username); - 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 cards = Cards.find(selector, { - fields: { - _id: 1, - archived: 1, - boardId: 1, - swimlaneId: 1, - listId: 1, - title: 1, - type: 1, - sort: 1, - members: 1, - assignees: 1, - colors: 1, - dueAt: 1, - }, - }); - - const boards = []; - const swimlanes = []; - const lists = []; - const users = []; - - cards.forEach(card => { - if (card.boardId) boards.push(card.boardId); - if (card.swimlaneId) swimlanes.push(card.swimlaneId); - if (card.listId) lists.push(card.listId); - if (card.members) { - card.members.forEach(userId => { - users.push(userId); - }); - } - if (card.assignees) { - card.assignees.forEach(userId => { - users.push(userId); - }); - } - }); - - return [ - cards, - Boards.find({ _id: { $in: boards } }), - Swimlanes.find({ _id: { $in: swimlanes } }), - Lists.find({ _id: { $in: lists } }), - Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), - ]; + return findCards(sessionId, buildQuery(queryParams)); }); -Meteor.publish('dueCards', function(allUsers = false) { - check(allUsers, Boolean); +// Meteor.publish('dueCards', function(sessionId, allUsers = false) { +// check(sessionId, String); +// check(allUsers, Boolean); +// +// // eslint-disable-next-line no-console +// // console.log('all users:', allUsers); +// +// const queryParams = { +// has: [{ field: 'dueAt', exists: true }], +// limit: 25, +// skip: 0, +// sort: { name: 'dueAt', order: 'des' }, +// }; +// +// if (!allUsers) { +// queryParams.users = [Meteor.user().username]; +// } +// +// return buildQuery(sessionId, queryParams); +// }); - // 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('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); + // console.log('queryParams:', params); + return findCards(sessionId, buildQuery(new QueryParams(params))); +}); + +function buildSelector(queryParams) { const userId = Meteor.userId(); - // eslint-disable-next-line no-console - // console.log('userId:', userId); - const errors = new (class { - constructor() { - this.notFound = { - boards: [], - swimlanes: [], - lists: [], - labels: [], - users: [], - members: [], - assignees: [], - status: [], - comments: [], - }; - - this.colorMap = Boards.colorMap(); - } - - hasErrors() { - for (const value of Object.values(this.notFound)) { - if (value.length) { - return true; - } - } - return false; - } - - errorMessages() { - const messages = []; - - this.notFound.boards.forEach(board => { - messages.push({ tag: 'board-title-not-found', value: board }); - }); - this.notFound.swimlanes.forEach(swim => { - messages.push({ tag: 'swimlane-title-not-found', value: swim }); - }); - this.notFound.lists.forEach(list => { - messages.push({ tag: 'list-title-not-found', value: list }); - }); - this.notFound.comments.forEach(comments => { - comments.forEach(text => { - messages.push({ tag: 'comment-not-found', value: text }); - }); - }); - this.notFound.labels.forEach(label => { - messages.push({ - tag: 'label-not-found', - value: label, - color: Boards.labelColors().includes(label), - }); - }); - this.notFound.users.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - this.notFound.members.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - this.notFound.assignees.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - - return messages; - } - })(); + const errors = new QueryErrors(); let selector = {}; - let skip = 0; - if (queryParams.skip) { - skip = queryParams.skip; - } - let limit = 25; - if (queryParams.limit) { - limit = queryParams.limit; - } if (queryParams.selector) { selector = queryParams.selector; @@ -267,15 +104,15 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { let archived = false; let endAt = null; - if (queryParams.status.length) { - queryParams.status.forEach(status => { - if (status === 'archived') { + if (queryParams.hasOperator(OPERATOR_STATUS)) { + queryParams.getPredicates(OPERATOR_STATUS).forEach(status => { + if (status === PREDICATE_ARCHIVED) { archived = true; - } 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; } }); @@ -320,9 +157,9 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { selector.endAt = endAt; } - if (queryParams.boards.length) { + if (queryParams.hasOperator(OPERATOR_BOARD)) { const queryBoards = []; - queryParams.boards.forEach(query => { + queryParams.hasOperator(OPERATOR_BOARD).forEach(query => { const boards = Boards.userSearch(userId, { title: new RegExp(escapeForRegex(query), 'i'), }); @@ -331,16 +168,16 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { queryBoards.push(board._id); }); } else { - errors.notFound.boards.push(query); + errors.addNotFound(OPERATOR_BOARD, query); } }); selector.boardId.$in = queryBoards; } - if (queryParams.swimlanes.length) { + if (queryParams.hasOperator(OPERATOR_SWIMLANE)) { const querySwimlanes = []; - queryParams.swimlanes.forEach(query => { + queryParams.getPredicates(OPERATOR_SWIMLANE).forEach(query => { const swimlanes = Swimlanes.find({ title: new RegExp(escapeForRegex(query), 'i'), }); @@ -349,7 +186,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { querySwimlanes.push(swim._id); }); } else { - errors.notFound.swimlanes.push(query); + errors.addNotFound(OPERATOR_SWIMLANE, query); } }); @@ -360,9 +197,9 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { selector.swimlaneId.$in = querySwimlanes; } - if (queryParams.lists.length) { + if (queryParams.hasOperator(OPERATOR_LIST)) { const queryLists = []; - queryParams.lists.forEach(query => { + queryParams.getPredicates(OPERATOR_LIST).forEach(query => { const lists = Lists.find({ title: new RegExp(escapeForRegex(query), 'i'), }); @@ -371,7 +208,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { queryLists.push(list._id); }); } else { - errors.notFound.lists.push(query); + errors.addNotFound(OPERATOR_LIST, query); } }); @@ -382,8 +219,10 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { selector.listId.$in = queryLists; } - if (queryParams.comments.length) { - const cardIds = CardComments.textSearch(userId, queryParams.comments).map( + if (queryParams.hasOperator(OPERATOR_COMMENT)) { + const cardIds = CardComments.textSearch( + userId, + queryParams.getPredicates(OPERATOR_COMMENT), com => { return com.cardId; }, @@ -391,82 +230,75 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { if (cardIds.length) { selector._id = { $in: cardIds }; } else { - errors.notFound.comments.push(queryParams.comments); + queryParams.getPredicates(OPERATOR_COMMENT).forEach(comment => { + errors.addNotFound(OPERATOR_COMMENT, comment); + }); } } - ['dueAt', 'createdAt', 'modifiedAt'].forEach(field => { - if (queryParams[field]) { + [OPERATOR_DUE, 'createdAt', 'modifiedAt'].forEach(field => { + if (queryParams.hasOperator(field)) { selector[field] = {}; - selector[field][queryParams[field]['operator']] = new Date( - queryParams[field]['value'], - ); + const predicate = queryParams.getPredicate(field); + selector[field][predicate.operator] = new Date(predicate.value); } }); - const queryMembers = []; - const queryAssignees = []; - if (queryParams.users.length) { - queryParams.users.forEach(query => { + const 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 => { - queryMembers.push(user._id); - queryAssignees.push(user._id); + queryUsers[OPERATOR_MEMBER].push(user._id); + queryUsers[OPERATOR_ASSIGNEE].push(user._id); }); } else { - errors.notFound.users.push(query); + errors.addNotFound(OPERATOR_USER, query); } }); } - if (queryParams.members.length) { - queryParams.members.forEach(query => { - const users = Users.find({ - username: query, + [OPERATOR_MEMBER, OPERATOR_ASSIGNEE].forEach(key => { + if (queryParams.hasOperator(key)) { + queryParams.getPredicates(key).forEach(query => { + const users = Users.find({ + username: query, + }); + if (users.count()) { + users.forEach(user => { + queryUsers[key].push(user._id); + }); + } else { + errors.addNotFound(key, query); + } }); - if (users.count()) { - users.forEach(user => { - queryMembers.push(user._id); - }); - } else { - errors.notFound.members.push(query); - } - }); - } + } + }); - if (queryParams.assignees.length) { - queryParams.assignees.forEach(query => { - const users = Users.find({ - username: query, - }); - if (users.count()) { - users.forEach(user => { - queryAssignees.push(user._id); - }); - } else { - errors.notFound.assignees.push(query); - } - }); - } - - if (queryMembers.length && queryAssignees.length) { + if ( + queryUsers[OPERATOR_MEMBER].length && + queryUsers[OPERATOR_ASSIGNEE].length + ) { selector.$and.push({ $or: [ - { members: { $in: queryMembers } }, - { assignees: { $in: queryAssignees } }, + { members: { $in: queryUsers[OPERATOR_MEMBER] } }, + { assignees: { $in: queryUsers[OPERATOR_ASSIGNEE] } }, ], }); - } else if (queryMembers.length) { - selector.members = { $in: queryMembers }; - } else if (queryAssignees.length) { - selector.assignees = { $in: queryAssignees }; + } else if (queryUsers[OPERATOR_MEMBER].length) { + selector.members = { $in: queryUsers[OPERATOR_MEMBER] }; + } else if (queryUsers[OPERATOR_ASSIGNEE].length) { + selector.assignees = { $in: queryUsers[OPERATOR_ASSIGNEE] }; } - if (queryParams.labels.length) { - queryParams.labels.forEach(label => { + if (queryParams.hasOperator(OPERATOR_LABEL)) { + queryParams.getPredicates(OPERATOR_LABEL).forEach(label => { const queryLabels = []; let boards = Boards.userSearch(userId, { @@ -511,39 +343,47 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { }); }); } else { - errors.notFound.labels.push(label); + errors.addNotFound(OPERATOR_LABEL, label); } } - selector.labelIds = { $in: queryLabels }; + selector.labelIds = { $in: _.uniq(queryLabels) }; }); } - if (queryParams.has.length) { - queryParams.has.forEach(has => { + if (queryParams.hasOperator(OPERATOR_HAS)) { + queryParams.getPredicates(OPERATOR_HAS).forEach(has => { switch (has.field) { - case 'attachment': - const attachments = Attachments.find({}, { fields: { cardId: 1 } }); + case PREDICATE_ATTACHMENT: selector.$and.push({ - _id: { $in: attachments.map(a => a.cardId) }, + _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) } }); + case PREDICATE_CHECKLIST: + selector.$and.push({ + _id: { + $in: Checklists.find({}, { fields: { cardId: 1 } }).map( + a => a.cardId, + ), + }, + }); 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 { @@ -573,26 +413,26 @@ Meteor.publish('globalSearch', function(sessionId, 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) } }, ], }); } @@ -607,6 +447,29 @@ Meteor.publish('globalSearch', function(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 = DEFAULT_LIMIT; + const configLimit = parseInt(process.env.RESULTS_PER_PAGE, 10); + if (!isNaN(configLimit) && configLimit > 0) { + limit = configLimit; + } + + if (query.params.hasOperator(OPERATOR_LIMIT)) { + limit = query.params.getPredicate(OPERATOR_LIMIT); + } + const projection = { fields: { _id: 1, @@ -636,10 +499,13 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { limit, }; - if (queryParams.sort) { - const order = queryParams.sort.order === 'asc' ? 1 : -1; - switch (queryParams.sort.name) { - case 'dueAt': + if (query.params.hasOperator(OPERATOR_SORT)) { + const order = + query.params.getPredicate(OPERATOR_SORT).order === ORDER_ASCENDING + ? 1 + : -1; + switch (query.params.getPredicate(OPERATOR_SORT).name) { + case PREDICATE_DUE_AT: projection.sort = { dueAt: order, boardId: 1, @@ -648,7 +514,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { sort: 1, }; break; - case 'modifiedAt': + case PREDICATE_MODIFIED_AT: projection.sort = { modifiedAt: order, boardId: 1, @@ -657,7 +523,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { sort: 1, }; break; - case 'createdAt': + case PREDICATE_CREATED_AT: projection.sort = { createdAt: order, boardId: 1, @@ -666,7 +532,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { sort: 1, }; break; - case 'system': + case PREDICATE_SYSTEM: projection.sort = { boardId: order, swimlaneId: order, @@ -681,77 +547,31 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) { // eslint-disable-next-line no-console // console.log('projection:', projection); - return findCards(sessionId, selector, projection, errors); -}); + query.projection = projection; -Meteor.publish('brokenCards', function() { - const user = Users.findOne({ _id: this.userId }); + return query; +} - const permiitedBoards = [null]; - let selector = {}; - selector.$or = [ - { permission: 'public' }, - { members: { $elemMatch: { userId: user._id, isActive: true } } }, +function buildQuery(queryParams) { + const query = buildSelector(queryParams); + + return buildProjection(query); +} + +Meteor.publish('brokenCards', function(sessionId) { + check(sessionId, String); + + const params = new QueryParams(); + params.addPredicate(OPERATOR_STATUS, PREDICATE_ALL); + const query = buildQuery(params); + query.selector.$or = [ + { boardId: { $in: [null, ''] } }, + { swimlaneId: { $in: [null, ''] } }, + { listId: { $in: [null, ''] } }, ]; + // console.log('brokenCards selector:', query.selector); - Boards.find(selector).forEach(board => { - permiitedBoards.push(board._id); - }); - - selector = { - boardId: { $in: permiitedBoards }, - $or: [ - { boardId: { $in: [null, ''] } }, - { swimlaneId: { $in: [null, ''] } }, - { listId: { $in: [null, ''] } }, - ], - }; - - const cards = Cards.find(selector, { - fields: { - _id: 1, - archived: 1, - boardId: 1, - swimlaneId: 1, - listId: 1, - title: 1, - type: 1, - sort: 1, - members: 1, - assignees: 1, - colors: 1, - dueAt: 1, - }, - }); - - const boards = []; - const swimlanes = []; - const lists = []; - const users = []; - - cards.forEach(card => { - if (card.boardId) boards.push(card.boardId); - if (card.swimlaneId) swimlanes.push(card.swimlaneId); - if (card.listId) lists.push(card.listId); - if (card.members) { - card.members.forEach(userId => { - users.push(userId); - }); - } - if (card.assignees) { - card.assignees.forEach(userId => { - users.push(userId); - }); - } - }); - - return [ - cards, - Boards.find({ _id: { $in: boards } }), - Swimlanes.find({ _id: { $in: swimlanes } }), - Lists.find({ _id: { $in: lists } }), - Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), - ]; + return findCards(sessionId, query); }); Meteor.publish('nextPage', function(sessionId) { @@ -761,7 +581,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) { @@ -771,19 +591,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 = null) { +function findCards(sessionId, query) { const userId = Meteor.userId(); // eslint-disable-next-line no-console - // console.log('selector:', selector); + // console.log('selector:', query.selector); + // console.log('selector.$and:', query.selector.$and); // eslint-disable-next-line no-console // console.log('projection:', projection); let cards; - if (!errors || !errors.hasErrors()) { - cards = Cards.find(selector, projection); + if (!query.hasErrors()) { + cards = Cards.find(query.selector, query.projection); } // eslint-disable-next-line no-console // console.log('count:', cards.count()); @@ -794,19 +615,17 @@ function findCards(sessionId, selector, projection, errors = null) { lastHit: 0, resultsCount: 0, cards: [], - selector: SessionData.pickle(selector), - projection: SessionData.pickle(projection), + selector: SessionData.pickle(query.selector), + projection: SessionData.pickle(query.projection), + errors: query.errors(), }, }; - if (errors) { - update.$set.errors = errors.errorMessages(); - } if (cards) { 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; @@ -884,5 +703,5 @@ function findCards(sessionId, selector, projection, errors = null) { ]; } - return [SessionData.find({ userId: userId, sessionId })]; + return [SessionData.find({ userId, sessionId })]; } 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"