diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 961c5b436..3dac5c9ef 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -14,7 +14,14 @@ template(name="globalSearch") if currentUser .wrapper form.global-search-instructions.js-search-query-form - input.global-search-query-input(type="text" name="searchQuery" placeholder="{{_ 'search-example'}}" value="{{ query.get }}" autofocus dir="auto") + input.global-search-query-input( + id="global-search-input" + type="text" + name="searchQuery" + placeholder="{{_ 'search-example'}}" + value="{{ query.get }}" + autofocus dir="auto" + ) if searching.get +spinner else if hasResults.get @@ -32,6 +39,26 @@ template(name="globalSearch") +resultCard(card) else .global-search-instructions + h2 {{_ 'boards' }} + .lists-wrapper + each title in myBoardNames.get + span.card-label.list-title.js-board-title + = title + h2 {{_ 'lists' }} + .lists-wrapper + each title in myLists.get + span.card-label.list-title.js-list-title + = title + h2 {{_ 'label-colors' }} + .palette-colors: each label in labelColors + span.card-label.palette-color.js-label-color(class="card-label-{{label.color}}") + = label.name + if myLabelNames.get.length + h2 {{_ 'label-names' }} + .lists-wrapper + each name in myLabelNames.get + span.card-label.list-title.js-label-name + = name +viewer = searchInstructions diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index d8e4134ca..8ad6aec2f 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -42,6 +42,9 @@ BlazeComponent.extendComponent({ this.query = new ReactiveVar(''); this.resultsHeading = new ReactiveVar(''); this.searchLink = new ReactiveVar(null); + this.myLists = new ReactiveVar([]); + this.myLabelNames = new ReactiveVar([]); + this.myBoardNames = new ReactiveVar([]); this.queryParams = null; this.parsingErrors = []; this.resultsCount = 0; @@ -55,6 +58,25 @@ BlazeComponent.extendComponent({ // } // // eslint-disable-next-line no-console // console.log('colorMap:', this.colorMap); + + Meteor.call('myLists', (err, data) => { + if (!err) { + this.myLists.set(data); + } + }); + + Meteor.call('myLabelNames', (err, data) => { + if (!err) { + this.myLabelNames.set(data); + } + }); + + Meteor.call('myBoardNames', (err, data) => { + if (!err) { + this.myBoardNames.set(data); + } + }); + Meteor.subscribe('setting'); if (Session.get('globalQuery')) { this.searchAllBoards(Session.get('globalQuery')); @@ -111,11 +133,13 @@ BlazeComponent.extendComponent({ messages.push({ tag: 'list-title-not-found', value: list }); }); this.queryErrors.notFound.labels.forEach(label => { - const color = TAPi18n.__(`color-${label}`); - if (color) { + const color = Object.entries(this.colorMap) + .filter(value => value[1] === label) + .map(value => value[0]); + if (color.length) { messages.push({ tag: 'label-color-not-found', - value: color, + value: color[0], }); } else { messages.push({ tag: 'label-not-found', value: label }); @@ -185,9 +209,12 @@ BlazeComponent.extendComponent({ operatorMap[TAPi18n.__('operator-assignee')] = 'assignees'; operatorMap[TAPi18n.__('operator-assignee-abbrev')] = 'assignees'; operatorMap[TAPi18n.__('operator-is')] = 'is'; + operatorMap[TAPi18n.__('operator-due')] = 'dueAt'; + operatorMap[TAPi18n.__('operator-created')] = 'createdAt'; + operatorMap[TAPi18n.__('operator-modified')] = 'modifiedAt'; // eslint-disable-next-line no-console - // console.log('operatorMap:', operatorMap); + console.log('operatorMap:', operatorMap); const params = { boards: [], swimlanes: [], @@ -197,6 +224,9 @@ BlazeComponent.extendComponent({ assignees: [], labels: [], is: [], + dueAt: null, + createdAt: null, + modifiedAt: null, }; let text = ''; @@ -223,8 +253,33 @@ BlazeComponent.extendComponent({ if (value in this.colorMap) { value = this.colorMap[value]; } + } else if ( + ['dueAt', 'createdAt', 'modifiedAt'].includes(operatorMap[op]) + ) { + const days = parseInt(value, 10); + if (isNaN(days)) { + if (['day', 'week', 'month', 'quarter', 'year'].includes(value)) { + value = moment() + .subtract(1, value) + .format(); + } else { + this.parsingErrors.push({ + tag: 'operator-number-expected', + value: { operator: op, value }, + }); + value = null; + } + } else { + value = moment() + .subtract(days, 'days') + .format(); + } + } + if (Array.isArray(params[operatorMap[op]])) { + params[operatorMap[op]].push(value); + } else { + params[operatorMap[op]] = value; } - params[operatorMap[op]].push(value); } else { this.parsingErrors.push({ tag: 'operator-unknown-error', @@ -355,6 +410,14 @@ BlazeComponent.extendComponent({ return text; }, + labelColors() { + return Boards.simpleSchema()._schema['labels.$.color'].allowedValues.map( + color => { + return { color, name: TAPi18n.__(`color-${color}`) }; + }, + ); + }, + events() { return [ { @@ -362,6 +425,42 @@ BlazeComponent.extendComponent({ evt.preventDefault(); this.searchAllBoards(evt.target.searchQuery.value); }, + 'click .js-label-color'(evt) { + evt.preventDefault(); + this.query.set( + `${this.query.get()} ${TAPi18n.__('operator-label')}:"${ + evt.currentTarget.textContent + }"`, + ); + document.getElementById('global-search-input').focus(); + }, + 'click .js-board-title'(evt) { + evt.preventDefault(); + this.query.set( + `${this.query.get()} ${TAPi18n.__('operator-board')}:"${ + evt.currentTarget.textContent + }"`, + ); + document.getElementById('global-search-input').focus(); + }, + 'click .js-list-title'(evt) { + evt.preventDefault(); + this.query.set( + `${this.query.get()} ${TAPi18n.__('operator-list')}:"${ + evt.currentTarget.textContent + }"`, + ); + document.getElementById('global-search-input').focus(); + }, + 'click .js-label-name'(evt) { + evt.preventDefault(); + this.query.set( + `${this.query.get()} ${TAPi18n.__('operator-label')}:"${ + evt.currentTarget.textContent + }"`, + ); + document.getElementById('global-search-input').focus(); + }, }, ]; }, diff --git a/client/components/main/globalSearch.styl b/client/components/main/globalSearch.styl index 4dc5b5f6d..b982f4eed 100644 --- a/client/components/main/globalSearch.styl +++ b/client/components/main/globalSearch.styl @@ -78,6 +78,12 @@ margin-left: auto line-height: 150% +.global-search-instructions h1 + margin-top: 2rem; + +.global-search-instructions h2 + margin-top: 1rem; + .global-search-query-input width: 90% !important margin-right: auto @@ -95,3 +101,6 @@ code background-color: lightgrey padding: 0.1rem !important font-size: 0.7rem !important + +.list-title + background-color: darkgray diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index b5d1df9c7..3e020dac7 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -895,7 +895,11 @@ "operator-assignee": "assignee", "operator-assignee-abbrev": "a", "operator-is": "is", + "operator-due": "due", + "operator-created": "created", + "operator-modified": "modified", "operator-unknown-error": "%s is not an operator", + "operator-number-expected": "operator __operator__ expected a number, got '__value__'", "heading-notes": "Notes", "globalSearch-instructions-heading": "Search Instructions", "globalSearch-instructions-description": "Searches can include operators to refine the search. Operators are specified by writing the operator name and value separated by a colon. For example, an operator specification of `list:Blocked` would limit the search to cards that are contained in a list named *Blocked*. If the value contains spaces or special characters it must be enclosed in quotation marks (e.g. `__operator_list__:\"To Review\"`).", @@ -916,5 +920,7 @@ "globalSearch-instructions-notes-5": "Currently archived cards are not searched.", "link-to-search": "Link to this search", "excel-font": "Arial", - "number": "Number" + "number": "Number", + "label-colors": "Label Colors", + "label-names": "Label Names" } diff --git a/models/boards.js b/models/boards.js index 006b58ed5..e0b791fa8 100644 --- a/models/boards.js +++ b/models/boards.js @@ -1343,6 +1343,26 @@ if (Meteor.isServer) { }, }); }, + myLabelNames() { + let names = []; + Boards.userBoards(Meteor.userId()).forEach(board => { + names = names.concat( + board.labels + .filter(label => !!label.name) + .map(label => { + return label.name; + }), + ); + }); + return _.uniq(names).sort(); + }, + myBoardNames() { + return _.uniq( + Boards.userBoards(Meteor.userId()).map(board => { + return board.title; + }), + ).sort(); + }, }); Meteor.methods({ diff --git a/models/cards.js b/models/cards.js index eea26b14d..13df86e3e 100644 --- a/models/cards.js +++ b/models/cards.js @@ -1954,6 +1954,18 @@ Cards.globalSearch = queryParams => { selector.listId.$in = queryLists; } + if (queryParams.dueAt !== null) { + selector.dueAt = { $gte: new Date(queryParams.dueAt) }; + } + + if (queryParams.createdAt !== null) { + selector.createdAt = { $gte: new Date(queryParams.createdAt) }; + } + + if (queryParams.modifiedAt !== null) { + selector.modifiedAt = { $gte: new Date(queryParams.modifiedAt) }; + } + const queryMembers = []; const queryAssignees = []; if (queryParams.users.length) { @@ -2079,7 +2091,7 @@ Cards.globalSearch = queryParams => { } // eslint-disable-next-line no-console - // console.log('selector:', selector); + console.log('selector:', selector); const cards = Cards.find(selector, { fields: { _id: 1, @@ -2094,13 +2106,15 @@ Cards.globalSearch = queryParams => { assignees: 1, colors: 1, dueAt: 1, + createdAt: 1, + modifiedAt: 1, labelIds: 1, }, limit: 50, }); // eslint-disable-next-line no-console - // console.log('count:', cards.count()); + console.log('count:', cards.count()); return { cards, errors }; }; diff --git a/models/lists.js b/models/lists.js index dcfd4294e..920221683 100644 --- a/models/lists.js +++ b/models/lists.js @@ -362,6 +362,20 @@ Meteor.methods({ const list = Lists.findOne({ _id: listId }); list.toggleSoftLimit(!list.getWipLimit('soft')); }, + + myLists() { + // my lists + return _.uniq( + Lists.find( + { boardId: { $in: Boards.userBoardIds(this.userId) } }, + { fields: { title: 1 } }, + ) + .fetch() + .map(list => { + return list.title; + }), + ).sort(); + }, }); Lists.hookOptions.after.update = { fetchPrevious: false };