diff --git a/client/components/cards/resultCard.jade b/client/components/cards/resultCard.jade index b1fabd9b6..77f0473af 100644 --- a/client/components/cards/resultCard.jade +++ b/client/components/cards/resultCard.jade @@ -1,6 +1,6 @@ template(name="resultCard") .result-card-wrapper - a.minicard-wrapper.card-title(href=card.absoluteUrl) + a.minicard-wrapper.card-title(href=absoluteUrl) +minicard(this) //= card.title ul.result-card-context-list diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 07920e7cf..961c5b436 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -14,52 +14,26 @@ 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'}}" autofocus dir="auto") + input.global-search-query-input(type="text" name="searchQuery" placeholder="{{_ 'search-example'}}" value="{{ query.get }}" autofocus dir="auto") if searching.get +spinner else if hasResults.get - .global-search-dueat-list-wrapper - h1 - if $eq resultsCount.get 0 - | {{_ 'no-cards-found' }} - else if $eq resultsCount.get 1 - | {{_ 'one-card-found' }} - else if $eq resultsCount.get totalHits.get - | {{_ 'n-cards-found' resultsCount.get }} - else - | {{_ 'n-n-of-n-cards-found' 1 resultsCount.get totalHits.get }} - if queryErrors.get + .global-search-results-list-wrapper + if hasQueryErrors.get div each msg in errorMessages span.global-search-error-messages | {{_ msg.tag msg.value }} - each card in results - a.minicard-wrapper(href=card.absoluteUrl) + else + h1 + = resultsHeading.get + a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}") + each card in results +resultCard(card) else .global-search-instructions - h1 Search Operators +viewer - = '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. `list:"To Review"`).\n' - = 'Available operators are:\n' - = '* `board:title` - cards in boards matching the specified title\n' - = '* `list:title` - cards in lists matching the specified title\n' - = '* `swimlane:title` - cards in swimlanes matching the specified title\n' - = '* `label:color` - cards that have a label matching the given color\n' - = '* `label:name` - cards that have a label matching the given name\n' - = '* `user:username` - cards where the specified user is a member or assignee\n' - = '* `@username` - shorthand for `user:username`\n' - = '* `#label` - shorthand for `label:color-or-name`\n' - = '## Notes\n' - = '* Multiple operators may be specified.\n' - = '* Similar operators are *OR*ed together. Cards that match any of the conditions will be returned.\n' - = ' `list:Available list:Blocked` would return cards contained in any list named *Blocked* or *Available*.\n' - = '* Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned.\n' - = '`list:Available label:red` returns only cards in the list *Available* with a *red* label.\n' - = '* Text searches are case insensitive.\n' + = searchInstructions template(name="globalSearchViewChangePopup") if currentUser diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 560cf9dda..51428c2e2 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -36,164 +36,300 @@ BlazeComponent.extendComponent({ BlazeComponent.extendComponent({ onCreated() { - this.isPageReady = new ReactiveVar(true); 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.queryParams = null; - this.resultsCount = new ReactiveVar(0); - this.totalHits = new ReactiveVar(0); - this.queryErrors = new ReactiveVar(null); + this.parsingErrors = []; + this.resultsCount = 0; + this.totalHits = 0; + this.queryErrors = null; Meteor.subscribe('setting'); + if (Session.get('globalQuery')) { + this.searchAllBoards(Session.get('globalQuery')); + } + }, + + resetSearch() { + this.searching.set(false); + this.hasResults.set(false); + this.hasQueryErrors.set(false); + this.resultsHeading.set(''); + this.parsingErrors = []; + this.resultsCount = 0; + this.totalHits = 0; + this.queryErrors = null; }, results() { + // eslint-disable-next-line no-console + // console.log('getting results'); if (this.queryParams) { const results = Cards.globalSearch(this.queryParams); + this.queryErrors = results.errors; // eslint-disable-next-line no-console - // console.log('user:', Meteor.user()); - // eslint-disable-next-line no-console - // console.log('user:', Meteor.user().sessionData); - // console.log('errors:', results.errors); - this.totalHits.set(Meteor.user().sessionData.totalHits); - this.resultsCount.set(results.cards.count()); - this.queryErrors.set(results.errors); - return results.cards; + // console.log('errors:', this.queryErrors); + if (this.errorMessages().length) { + this.hasQueryErrors.set(true); + return null; + } + + if (results.cards) { + const sessionData = SessionData.findOne({ userId: Meteor.userId() }); + this.totalHits = sessionData.totalHits; + this.resultsCount = results.cards.count(); + this.resultsHeading.set(this.getResultsHeading()); + return results.cards; + } } - this.resultsCount.set(0); + this.resultsCount = 0; return []; }, errorMessages() { - const errors = this.queryErrors.get(); const messages = []; - errors.notFound.boards.forEach(board => { - messages.push({ tag: 'board-title-not-found', value: board }); - }); - errors.notFound.swimlanes.forEach(swim => { - messages.push({ tag: 'swimlane-title-not-found', value: swim }); - }); - errors.notFound.lists.forEach(list => { - messages.push({ tag: 'list-title-not-found', value: list }); - }); - errors.notFound.users.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); + if (this.queryErrors) { + this.queryErrors.notFound.boards.forEach(board => { + messages.push({ tag: 'board-title-not-found', value: board }); + }); + this.queryErrors.notFound.swimlanes.forEach(swim => { + messages.push({ tag: 'swimlane-title-not-found', value: swim }); + }); + this.queryErrors.notFound.lists.forEach(list => { + messages.push({ tag: 'list-title-not-found', value: list }); + }); + this.queryErrors.notFound.labels.forEach(label => { + messages.push({ tag: 'label-not-found', value: label }); + }); + this.queryErrors.notFound.users.forEach(user => { + messages.push({ tag: 'user-username-not-found', value: user }); + }); + this.queryErrors.notFound.members.forEach(user => { + messages.push({ tag: 'user-username-not-found', value: user }); + }); + this.queryErrors.notFound.assignees.forEach(user => { + messages.push({ tag: 'user-username-not-found', value: user }); + }); + } + + if (this.parsingErrors.length) { + this.parsingErrors.forEach(err => { + messages.push(err); + }); + } return messages; }, + searchAllBoards(query) { + this.query.set(query); + + this.resetSearch(); + + if (!query) { + return; + } + + this.searching.set(true); + + // eslint-disable-next-line no-console + // console.log('query:', query); + + const reOperator1 = /^((?\w+):|(?[#@]))(?\w+)(\s+|$)/; + const reOperator2 = /^((?\w+):|(?[#@]))(?["']*)(?.*?)\k(\s+|$)/; + const reText = /^(?\S+)(\s+|$)/; + const reQuotedText = /^(?["'])(?\w+)\k(\s+|$)/; + + const operatorMap = {}; + operatorMap[TAPi18n.__('operator-board')] = 'boards'; + operatorMap[TAPi18n.__('operator-board-abbrev')] = 'boards'; + operatorMap[TAPi18n.__('operator-swimlane')] = 'swimlanes'; + operatorMap[TAPi18n.__('operator-swimlane-abbrev')] = 'swimlanes'; + operatorMap[TAPi18n.__('operator-list')] = 'lists'; + operatorMap[TAPi18n.__('operator-list-abbrev')] = 'lists'; + operatorMap[TAPi18n.__('operator-label')] = 'labels'; + operatorMap[TAPi18n.__('operator-label-abbrev')] = 'labels'; + operatorMap[TAPi18n.__('operator-user')] = 'users'; + operatorMap[TAPi18n.__('operator-user-abbrev')] = 'users'; + operatorMap[TAPi18n.__('operator-member')] = 'members'; + operatorMap[TAPi18n.__('operator-member-abbrev')] = 'members'; + operatorMap[TAPi18n.__('operator-assignee')] = 'assignees'; + operatorMap[TAPi18n.__('operator-assignee-abbrev')] = 'assignees'; + operatorMap[TAPi18n.__('operator-is')] = 'is'; + + // eslint-disable-next-line no-console + // console.log('operatorMap:', operatorMap); + const params = { + boards: [], + swimlanes: [], + lists: [], + users: [], + members: [], + assignees: [], + labels: [], + is: [], + }; + + 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; + } + if (op in operatorMap) { + params[operatorMap[op]].push(m.groups.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; + } + } + + // eslint-disable-next-line no-console + // console.log('text:', text); + params.text = text; + + // eslint-disable-next-line no-console + // console.log('params:', params); + + this.queryParams = params; + + this.autorun(() => { + const handle = subManager.subscribe('globalSearch', params); + Tracker.nonreactive(() => { + Tracker.autorun(() => { + // eslint-disable-next-line no-console + // console.log('ready:', handle.ready()); + if (handle.ready()) { + 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: 1, + end: this.resultsCount, + total: this.totalHits, + }); + }, + + getSearchHref() { + const baseUrl = window.location.href.replace(/([?#].*$|\s*$)/, ''); + return `${baseUrl}?q=${encodeURIComponent(this.query.get())}`; + }, + + searchInstructions() { + tags = { + operator_board: TAPi18n.__('operator-board'), + operator_list: TAPi18n.__('operator-list'), + operator_swimlane: TAPi18n.__('operator-swimlane'), + operator_label: TAPi18n.__('operator-label'), + operator_label_abbrev: TAPi18n.__('operator-label-abbrev'), + operator_user: TAPi18n.__('operator-user'), + operator_user_abbrev: TAPi18n.__('operator-user-abbrev'), + operator_member: TAPi18n.__('operator-member'), + operator_member_abbrev: TAPi18n.__('operator-member-abbrev'), + operator_assignee: TAPi18n.__('operator-assignee'), + operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'), + }; + + text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; + text += `\n${TAPi18n.__('globalSearch-instructions-description', tags)}`; + text += `\n${TAPi18n.__('globalSearch-instructions-operators', tags)}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-board', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-list', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-swimlane', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-label', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-hash', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-user', + tags, + )}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-at', tags)}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-member', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-assignee', + tags, + )}`; + + text += `\n## ${TAPi18n.__('heading-notes')}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-4', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-5', tags)}`; + + return text; + }, + events() { return [ { 'submit .js-search-query-form'(evt) { evt.preventDefault(); - this.query.set(evt.target.searchQuery.value); - this.queryErrors.set(null); - - if (!this.query.get()) { - this.searching.set(false); - this.hasResults.set(false); - return; - } - - this.searching.set(true); - this.hasResults.set(false); - - let query = this.query.get(); - // eslint-disable-next-line no-console - // console.log('query:', query); - - const reOperator1 = /^((?\w+):|(?[#@]))(?\w+)(\s+|$)/; - const reOperator2 = /^((?\w+):|(?[#@]))(?["']*)(?.*?)\k(\s+|$)/; - const reText = /^(?\S+)(\s+|$)/; - const reQuotedText = /^(?["'])(?\w+)\k(\s+|$)/; - - const operatorMap = {}; - operatorMap[TAPi18n.__('operator-board')] = 'boards'; - operatorMap[TAPi18n.__('operator-board-abbrev')] = 'boards'; - operatorMap[TAPi18n.__('operator-swimlane')] = 'swimlanes'; - operatorMap[TAPi18n.__('operator-swimlane-abbrev')] = 'swimlanes'; - operatorMap[TAPi18n.__('operator-list')] = 'lists'; - operatorMap[TAPi18n.__('operator-list-abbrev')] = 'lists'; - operatorMap[TAPi18n.__('operator-label')] = 'labels'; - operatorMap[TAPi18n.__('operator-label-abbrev')] = 'labels'; - operatorMap[TAPi18n.__('operator-user')] = 'users'; - operatorMap[TAPi18n.__('operator-user-abbrev')] = 'users'; - operatorMap[TAPi18n.__('operator-is')] = 'is'; - - // eslint-disable-next-line no-console - // console.log('operatorMap:', operatorMap); - const params = { - boards: [], - swimlanes: [], - lists: [], - users: [], - labels: [], - is: [], - }; - - 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; - } - if (op in operatorMap) { - params[operatorMap[op]].push(m.groups.value); - } - 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; - } - } - - // eslint-disable-next-line no-console - // console.log('text:', text); - params.text = text; - - // eslint-disable-next-line no-console - // console.log('params:', params); - - this.queryParams = params; - - this.autorun(() => { - const handle = subManager.subscribe('globalSearch', params); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - // eslint-disable-next-line no-console - // console.log('ready:', handle.ready()); - if (handle.ready()) { - this.searching.set(false); - this.hasResults.set(true); - } - }); - }); - }); + this.searchAllBoards(evt.target.searchQuery.value); }, }, ]; diff --git a/client/components/main/globalSearch.styl b/client/components/main/globalSearch.styl index 20bb45138..4dc5b5f6d 100644 --- a/client/components/main/globalSearch.styl +++ b/client/components/main/globalSearch.styl @@ -51,7 +51,7 @@ margin-top: 0 margin-bottom: 10px -.global-search-dueat-list-wrapper +.global-search-results-list-wrapper max-width: 500px margin-right: auto margin-left: auto @@ -91,7 +91,7 @@ font-style: italic code - color: white - background-color: grey + color: black + background-color: lightgrey padding: 0.1rem !important font-size: 0.7rem !important diff --git a/config/router.js b/config/router.js index 4d5a386f9..6aca98570 100644 --- a/config/router.js +++ b/config/router.js @@ -158,7 +158,14 @@ FlowRouter.route('/global-search', { Utils.manageCustomUI(); Utils.manageMatomo(); + DocHead.setTitle(TAPi18n.__('globalSearch-title')); + if (FlowRouter.getQueryParam('q')) { + Session.set( + 'globalQuery', + decodeURIComponent(FlowRouter.getQueryParam('q')), + ); + } BlazeLayout.render('defaultLayout', { headerBar: 'globalSearchHeaderBar', content: 'globalSearch', diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index c4a22427e..b1859ff2b 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -868,12 +868,13 @@ "board-title-not-found": "Board '%s' not found.", "swimlane-title-not-found": "Swimlane '%s' not found.", "list-title-not-found": "List '%s' not found.", + "label-not-found": "Label '%s' not found.", "user-username-not-found": "Username '%s' not found.", "globalSearch-title": "Search All Boards", "no-cards-found": "No Cards Found", "one-card-found": "One Card Found", "n-cards-found": "%s Cards Found", - "n-n-of-n-cards-found": "%s-%s of %s Cards Found", + "n-n-of-n-cards-found": "__start__-__end__ of __total__ Cards Found", "operator-board": "board", "operator-board-abbrev": "b", "operator-swimlane": "swimlane", @@ -884,5 +885,29 @@ "operator-label-abbrev": "#", "operator-user": "user", "operator-user-abbrev": "@", - "operator-is": "is" + "operator-member": "member", + "operator-member-abbrev": "m", + "operator-assignee": "assignee", + "operator-assignee-abbrev": "a", + "operator-is": "is", + "operator-unknown-error": "%s is not an operator", + "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\"`).", + "globalSearch-instructions-operators": "Available operators:", + "globalSearch-instructions-operator-board": "`__operator_board__:title` - cards in boards matching the specified title", + "globalSearch-instructions-operator-list": "`__operator_list__:title` - cards in lists matching the specified title", + "globalSearch-instructions-operator-swimlane": "`__operator_swimlane__:title` - cards in swimlanes matching the specified title", + "globalSearch-instructions-operator-label": "`__operator_label__:color` `__operator_label__:name` - cards that have a label matching the given color or name", + "globalSearch-instructions-operator-hash": "`__operator_label_abbrev__label` - shorthand for `__operator_label__:label`", + "globalSearch-instructions-operator-user": "`__operator_user__:username` - cards where the specified user is a *member* or *assignee*", + "globalSearch-instructions-operator-at": "`__operator_user_abbrev__username` - shorthand for `user:username`", + "globalSearch-instructions-operator-member": "`__operator_member__:username` - cards where the specified user is a *member*", + "globalSearch-instructions-operator-assignee": "`__operator_assignee__:username` - cards where the specified user is an *assignee*", + "globalSearch-instructions-notes-1": "Multiple operators may be specified.", + "globalSearch-instructions-notes-2": "Similar operators are *OR*ed together. Cards that match any of the conditions will be returned.\n`__operator_list__:Available __operator_list__:Blocked` would return cards contained in any list named *Blocked* or *Available*.", + "globalSearch-instructions-notes-3": "Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned.\n`__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.", + "globalSearch-instructions-notes-4": "Text searches are case insensitive.", + "globalSearch-instructions-notes-5": "Currently archived cards are not searched.", + "link-to-search": "Link to this search" } diff --git a/models/cards.js b/models/cards.js index cce9d98cd..5358f41b7 100644 --- a/models/cards.js +++ b/models/cards.js @@ -1866,16 +1866,29 @@ Cards.globalSearch = queryParams => { // eslint-disable-next-line no-console // console.log('userId:', userId); - const errors = { - notFound: { - boards: [], - swimlanes: [], - lists: [], - labels: [], - users: [], - is: [], - }, - }; + const errors = new (class { + constructor() { + this.notFound = { + boards: [], + swimlanes: [], + lists: [], + labels: [], + users: [], + members: [], + assignees: [], + is: [], + }; + } + + hasErrors() { + for (const prop in this.notFound) { + if (this.notFound[prop].length) { + return true; + } + } + return false; + } + })(); const selector = { archived: false, @@ -1939,25 +1952,63 @@ Cards.globalSearch = queryParams => { selector.listId.$in = queryLists; } + const queryMembers = []; + const queryAssignees = []; if (queryParams.users.length) { - const queryUsers = []; queryParams.users.forEach(query => { const users = Users.find({ username: query, }); if (users.count()) { users.forEach(user => { - queryUsers.push(user._id); + queryMembers.push(user._id); + queryAssignees.push(user._id); }); } else { errors.notFound.users.push(query); } }); + } + if (queryParams.members.length) { + queryParams.members.forEach(query => { + const users = Users.find({ + username: 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) { selector.$or = [ - { members: { $in: queryUsers } }, - { assignees: { $in: queryUsers } }, + { members: { $in: queryMembers } }, + { assignees: { $in: queryAssignees } }, ]; + } else if (queryMembers.length) { + selector.members = { $in: queryMembers }; + } else if (queryAssignees.length) { + selector.assignees = { $in: queryAssignees }; } if (queryParams.labels.length) { @@ -2003,7 +2054,7 @@ Cards.globalSearch = queryParams => { }); }); } else { - errors.notFound.labels.push({ tag: 'label', value: label }); + errors.notFound.labels.push(label); } } @@ -2011,6 +2062,10 @@ Cards.globalSearch = queryParams => { }); } + if (errors.hasErrors()) { + return { cards: null, errors }; + } + if (queryParams.text) { const regex = new RegExp(queryParams.text, 'i'); @@ -2045,14 +2100,6 @@ Cards.globalSearch = queryParams => { // eslint-disable-next-line no-console // console.log('count:', cards.count()); - if (Meteor.isServer) { - Users.update(userId, { - $set: { - 'sessionData.totalHits': cards.count(), - 'sessionData.lastHit': cards.count() > 50 ? 50 : cards.count(), - }, - }); - } return { cards, errors }; }; diff --git a/models/users.js b/models/users.js index 8b0c13230..d6bf22c27 100644 --- a/models/users.js +++ b/models/users.js @@ -377,6 +377,14 @@ Users.initEasySearch(searchInFields, { returnFields: [...searchInFields, 'profile.avatarUrl'], }); +Users.safeFields = { + _id: 1, + username: 1, + 'profile.fullname': 1, + 'profile.avatarUrl': 1, + 'profile.initials': 1, +}; + if (Meteor.isClient) { Users.helpers({ isBoardMember() { diff --git a/models/usersessiondata.js b/models/usersessiondata.js new file mode 100644 index 000000000..e93cde2bd --- /dev/null +++ b/models/usersessiondata.js @@ -0,0 +1,73 @@ +SessionData = new Mongo.Collection('sessiondata'); + +/** + * A UserSessionData in Wekan. Organization in Trello. + */ +SessionData.attachSchema( + new SimpleSchema({ + _id: { + /** + * the organization id + */ + type: Number, + optional: true, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert && !this.isSet) { + return incrementCounter('counters', 'orgId', 1); + } + }, + }, + userId: { + /** + * userId of the user + */ + type: String, + optional: false, + }, + totalHits: { + /** + * total number of hits in the last report query + */ + type: Number, + optional: true, + }, + lastHit: { + /** + * the last hit returned from a report query + */ + type: Number, + optional: true, + }, + createdAt: { + /** + * creation date of the team + */ + type: Date, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert) { + return new Date(); + } else if (this.isUpsert) { + return { $setOnInsert: new Date() }; + } else { + this.unset(); + } + }, + }, + modifiedAt: { + type: Date, + denyUpdate: false, + // eslint-disable-next-line consistent-return + autoValue() { + if (this.isInsert || this.isUpsert || this.isUpdate) { + return new Date(); + } else { + this.unset(); + } + }, + }, + }), +); + +export default SessionData; diff --git a/server/publications/cards.js b/server/publications/cards.js index ba60d3dc1..6d5223c50 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -72,18 +72,7 @@ Meteor.publish('myCards', function() { Boards.find({ _id: { $in: boards } }), Swimlanes.find({ _id: { $in: swimlanes } }), Lists.find({ _id: { $in: lists } }), - Users.find( - { _id: { $in: users } }, - { - fields: { - _id: 1, - username: 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - 'profile.initials': 1, - }, - }, - ), + Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), ]; }); @@ -93,18 +82,7 @@ Meteor.publish('dueCards', function(allUsers = false) { // eslint-disable-next-line no-console // console.log('all users:', allUsers); - const user = Users.findOne( - { _id: this.userId }, - { - fields: { - _id: 1, - username: 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - 'profile.initials': 1, - }, - }, - ); + const user = Users.findOne({ _id: this.userId }); const archivedBoards = []; Boards.find({ archived: true }).forEach(board => { @@ -115,14 +93,12 @@ Meteor.publish('dueCards', function(allUsers = false) { let selector = { archived: false, }; - // for admins and users, allow her to see cards only from boards 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); }); @@ -193,18 +169,7 @@ Meteor.publish('dueCards', function(allUsers = false) { Boards.find({ _id: { $in: boards } }), Swimlanes.find({ _id: { $in: swimlanes } }), Lists.find({ _id: { $in: lists } }), - Users.find( - { _id: { $in: users } }, - { - fields: { - _id: 1, - username: 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - 'profile.initials': 1, - }, - }, - ), + Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), ]; }); @@ -216,6 +181,20 @@ Meteor.publish('globalSearch', function(queryParams) { const cards = Cards.globalSearch(queryParams).cards; + if (!cards) { + return []; + } + + SessionData.upsert( + { userId: this.userId }, + { + $set: { + totalHits: cards.count(), + lastHit: cards.count() > 50 ? 50 : cards.count(), + }, + }, + ); + const boards = []; const swimlanes = []; const lists = []; @@ -244,34 +223,21 @@ Meteor.publish('globalSearch', function(queryParams) { Boards.find({ _id: { $in: boards } }), Swimlanes.find({ _id: { $in: swimlanes } }), Lists.find({ _id: { $in: lists } }), - Users.find({ _id: { $in: users } }), + Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), + SessionData.find({ userId: this.userId }), ]; }); Meteor.publish('brokenCards', function() { - const user = Users.findOne( - { _id: this.userId }, - { - fields: { - _id: 1, - username: 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - 'profile.initials': 1, - }, - }, - ); + const user = Users.findOne({ _id: this.userId }); const permiitedBoards = [null]; let selector = {}; - // for admins and users, if user is not an admin allow her to see cards only from boards 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); }); @@ -328,17 +294,6 @@ Meteor.publish('brokenCards', function() { Boards.find({ _id: { $in: boards } }), Swimlanes.find({ _id: { $in: swimlanes } }), Lists.find({ _id: { $in: lists } }), - Users.find( - { _id: { $in: users } }, - { - fields: { - _id: 1, - username: 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - 'profile.initials': 1, - }, - }, - ), + Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), ]; });