diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 31e9584e4..4b1faa718 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -42,6 +42,24 @@ template(name="globalSearch") autofocus dir="auto" ) a.js-new-search.fa.fa-eraser + if debug.get.show + h1 Debug + if debug.get.showSelector + h2 Selector + button.js-copy-debug-selector + = 'Copy' + pre( + id="debug-selector" + ) + = sessionData.selector + if debug.get.showProjection + h2 Projection + button.js-copy-debug-projection + = 'Copy' + pre( + id="debug-projection" + ) + = sessionData.projection if searching.get +spinner else if hasResults.get diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index f032c89ed..5150c49ff 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -222,6 +222,30 @@ class GlobalSearchComponent extends CardSearchPagedComponent { ); document.getElementById('global-search-input').focus(); }, + 'click .js-copy-debug-selector'(evt) { + /* Get the text field */ + const selector = document.getElementById("debug-selector"); + + try { + navigator.clipboard.writeText(selector.textContent); + alert("Selector copied to clipboard"); + } catch(err) { + alert("Error copying text: " + err); + } + + }, + 'click .js-copy-debug-projection'(evt) { + /* Get the text field */ + const projection = document.getElementById("debug-projection"); + + try { + navigator.clipboard.writeText(projection.textContent); + alert("Projection copied to clipboard"); + } catch(err) { + alert("Error copying text: " + err); + } + + }, 'click .js-board-title'(evt) { evt.preventDefault(); const input = document.getElementById('global-search-input'); diff --git a/client/components/settings/adminReports.jade b/client/components/settings/adminReports.jade index ae32a1aa6..7a1b8cb96 100644 --- a/client/components/settings/adminReports.jade +++ b/client/components/settings/adminReports.jade @@ -1,5 +1,5 @@ template(name="adminReports") - .setting-content + .setting-content.admin-reports-content unless currentUser.isAdmin | {{_ 'error-notAuthorized'}} else @@ -26,6 +26,16 @@ template(name="adminReports") i.fa.fa-magic | {{_ 'rulesReportTitle'}} + li + a.js-report-boards(data-id="report-boards") + i.fa.fa-magic + | {{_ 'boardsReportTitle'}} + + li + a.js-report-cards(data-id="report-cards") + i.fa.fa-magic + | {{_ 'cardsReportTitle'}} + .main-body if loading.get +spinner @@ -37,6 +47,10 @@ template(name="adminReports") +orphanedFilesReport else if showRulesReport.get +rulesReport + else if showBoardsReport.get + +boardsReport + else if showCardsReport.get + +cardsReport template(name="brokenCardsReport") @@ -57,7 +71,7 @@ template(name="rulesReport") th actionType th activityType - each rule in rows + each rule in results tr td {{ rule.title }} td {{ rule.boardTitle }} @@ -78,7 +92,7 @@ template(name="filesReport") th MD5 Sum th ID - each att in attachmentFiles + each att in results tr td {{ att.filename }} td.right {{fileSize att.length }} @@ -100,7 +114,7 @@ template(name="orphanedFilesReport") th MD5 Sum th ID - each att in attachmentFiles + each att in results tr td {{ att.filename }} td.right {{fileSize att.length }} @@ -109,3 +123,50 @@ template(name="orphanedFilesReport") td {{ att._id.toHexString }} else div {{_ 'no-results' }} + +template(name="cardsReport") + h1 {{_ 'cardsReportTitle'}} + if resultsCount + table.table + tr + th Card Title + th Board + th Swimlane + th List + th Members + th Assignees + + each card in results + tr + td {{abbreviate card.title }} + td {{abbreviate card.board.title }} + td {{abbreviate card.swimlane.title }} + td {{abbreviate card.list.title }} + td {{userNames card.members }} + td {{userNames card.assignees }} + else + div {{_ 'no-results' }} + +template(name="boardsReport") + h1 {{_ 'boardsReportTitle'}} + if resultsCount + table.table + tr + th Title + th Id + th Permission + th Archived? + th Members + th Organizations + th Teams + + each board in results + tr + td {{abbreviate board.title }} + td {{abbreviate board._id }} + td {{ board.permission }} + td + = yesOrNo(board.archived) + td {{userNames board.members }} + else + div {{_ 'no-results' }} diff --git a/client/components/settings/adminReports.js b/client/components/settings/adminReports.js index e8ba75fc0..6dcbb0fc4 100644 --- a/client/components/settings/adminReports.js +++ b/client/components/settings/adminReports.js @@ -1,6 +1,8 @@ import { AttachmentStorage } from '/models/attachments'; import { CardSearchPagedComponent } from '/client/lib/cardSearch'; import SessionData from '/models/usersessiondata'; +import { QueryParams } from '/config/query-classes'; +import { OPERATOR_LIMIT } from '/config/search-const'; BlazeComponent.extendComponent({ subscription: null, @@ -8,10 +10,14 @@ BlazeComponent.extendComponent({ showBrokenCardsReport: new ReactiveVar(false), showOrphanedFilesReport: new ReactiveVar(false), showRulesReport: new ReactiveVar(false), + showCardsReport: new ReactiveVar(false), + showBoardsReport: new ReactiveVar(false), + sessionId: null, onCreated() { this.error = new ReactiveVar(''); this.loading = new ReactiveVar(false); + this.sessionId = SessionData.getSessionId(); }, events() { @@ -21,6 +27,8 @@ BlazeComponent.extendComponent({ 'click a.js-report-files': this.switchMenu, 'click a.js-report-orphaned-files': this.switchMenu, 'click a.js-report-rules': this.switchMenu, + 'click a.js-report-cards': this.switchMenu, + 'click a.js-report-boards': this.switchMenu, }, ]; }, @@ -32,6 +40,9 @@ BlazeComponent.extendComponent({ this.showFilesReport.set(false); this.showBrokenCardsReport.set(false); this.showOrphanedFilesReport.set(false); + this.showRulesReport.set(false) + this.showBoardsReport.set(false); + this.showCardsReport.set(false); if (this.subscription) { this.subscription.stop(); } @@ -64,68 +75,79 @@ BlazeComponent.extendComponent({ this.showRulesReport.set(true); this.loading.set(false); }); + } else if ('report-boards' === targetID) { + this.subscription = Meteor.subscribe('boardsReport', () => { + this.showBoardsReport.set(true); + this.loading.set(false); + }); + } else if ('report-cards' === targetID) { + const qp = new QueryParams(); + qp.addPredicate(OPERATOR_LIMIT, 300); + this.subscription = Meteor.subscribe( + 'globalSearch', + this.sessionId, + qp.getParams(), + qp.text, + () => { + this.showCardsReport.set(true); + this.loading.set(false); + }, + ); } } }, }).register('adminReports'); -Template.filesReport.helpers({ - attachmentFiles() { +class AdminReport extends BlazeComponent { + collection; + + results() { // eslint-disable-next-line no-console // console.log('attachments:', AttachmentStorage.find()); // console.log('attachments.count:', AttachmentStorage.find().count()); - return AttachmentStorage.find(); - }, + return this.collection.find(); + } - rulesReport() { - const rules = []; - - Rules.find().forEach(rule => { - rules.push({ - _id: rule._id, - title: rule.title, - boardId: rule.boardId, - boardTitle: rule.board().title, - action: rule.action().fetch(), - trigger: rule.trigger().fetch(), - }); - }); - - return rules; - }, + yesOrNo(value) { + if (value) { + return TAPi18n.__('yes'); + } else { + return TAPi18n.__('no'); + } + } resultsCount() { - return AttachmentStorage.find().count(); - }, + return this.collection.find().count(); + } fileSize(size) { return Math.round(size / 1024); - }, + } usageCount(key) { return Attachments.find({ 'copies.attachments.key': key }).count(); - }, -}); + } -Template.orphanedFilesReport.helpers({ - attachmentFiles() { - // eslint-disable-next-line no-console - // console.log('attachments:', AttachmentStorage.find()); - // console.log('attachments.count:', AttachmentStorage.find().count()); - return AttachmentStorage.find(); - }, + abbreviate(text) { + if (text.length > 30) { + return `${text.substr(0, 29)}...`; + } + return text; + } +} - resultsCount() { - return AttachmentStorage.find().count(); - }, +(class extends AdminReport { + collection = AttachmentStorage; +}.register('filesReport')); - fileSize(size) { - return Math.round(size / 1024); - }, -}); +(class extends AdminReport { + collection = AttachmentStorage; +}.register('orphanedFilesReport')); -Template.rulesReport.helpers({ - rows() { +(class extends AdminReport { + collection = Rules; + + results() { const rules = []; Rules.find().forEach(rule => { @@ -139,14 +161,43 @@ Template.rulesReport.helpers({ }); }); + // eslint-disable-next-line no-console console.log('rows:', rules); return rules; - }, + } +}.register('rulesReport')); - resultsCount() { - return Rules.find().count(); - }, -}); +(class extends AdminReport { + collection = Boards; + + userNames(members) { + let text = ''; + members.forEach(member => { + const user = Users.findOne(member.userId); + text += text ? ', ' : ''; + if (user) { + text += user.username; + } else { + text += member.userId + } + }); + return text; + } +}.register('boardsReport')); + +(class extends AdminReport { + collection = Cards; + + userNames(userIds) { + let text = ''; + userIds.forEach(userId => { + const user = Users.findOne(userId); + text += text ? ', ' : ''; + text += user.username; + }); + return text; + } +}.register('cardsReport')); class BrokenCardsComponent extends CardSearchPagedComponent { onCreated() { diff --git a/client/components/settings/adminReports.styl b/client/components/settings/adminReports.styl new file mode 100644 index 000000000..3a5234842 --- /dev/null +++ b/client/components/settings/adminReports.styl @@ -0,0 +1,3 @@ +.admin-reports-content + height: auto !important + diff --git a/client/lib/cardSearch.js b/client/lib/cardSearch.js index 6daada3c8..7fe74492b 100644 --- a/client/lib/cardSearch.js +++ b/client/lib/cardSearch.js @@ -1,5 +1,7 @@ import Cards from '../../models/cards'; import SessionData from '../../models/usersessiondata'; +import {QueryDebug} from "../../config/query-classes"; +import {OPERATOR_DEBUG} from "../../config/search-const"; export class CardSearchPagedComponent extends BlazeComponent { onCreated() { @@ -19,6 +21,8 @@ export class CardSearchPagedComponent extends BlazeComponent { this.sessionId = SessionData.getSessionId(); this.subscriptionHandle = null; this.serverError = new ReactiveVar(false); + this.sessionData = null; + this.debug = new ReactiveVar(new QueryDebug()); const that = this; this.subscriptionCallbacks = { @@ -52,6 +56,7 @@ export class CardSearchPagedComponent extends BlazeComponent { this.resultsCount = 0; this.totalHits = 0; this.queryErrors = null; + this.debug.set(new QueryDebug()); } getSessionData(sessionId) { @@ -63,30 +68,34 @@ export class CardSearchPagedComponent extends BlazeComponent { getResults() { // eslint-disable-next-line no-console // console.log('getting results'); - const sessionData = this.getSessionData(); + this.sessionData = this.getSessionData(); // eslint-disable-next-line no-console - console.log('session data:', sessionData); + console.log('session data:', this.sessionData); const cards = []; - sessionData.cards.forEach(cardId => { + this.sessionData.cards.forEach(cardId => { cards.push(Cards.findOne({ _id: cardId })); }); - this.queryErrors = sessionData.errors; + this.queryErrors = this.sessionData.errors; if (this.queryErrors.length) { // console.log('queryErrors:', this.queryErrorMessages()); this.hasQueryErrors.set(true); // return null; } + this.debug.set(new QueryDebug(this.sessionData.debug)); + console.log('debug:', this.debug.get().get()); + console.log('debug.show():', this.debug.get().show()); + console.log('debug.showSelector():', this.debug.get().showSelector()); if (cards) { - this.totalHits = sessionData.totalHits; + this.totalHits = this.sessionData.totalHits; this.resultsCount = cards.length; - this.resultsStart = sessionData.lastHit - this.resultsCount + 1; - this.resultsEnd = sessionData.lastHit; + this.resultsStart = this.sessionData.lastHit - this.resultsCount + 1; + this.resultsEnd = this.sessionData.lastHit; this.resultsHeading.set(this.getResultsHeading()); this.results.set(cards); - this.hasNextPage.set(sessionData.lastHit < sessionData.totalHits); + this.hasNextPage.set(this.sessionData.lastHit < this.sessionData.totalHits); this.hasPreviousPage.set( - sessionData.lastHit - sessionData.resultsCount > 0, + this.sessionData.lastHit - this.sessionData.resultsCount > 0, ); return cards; } @@ -113,6 +122,7 @@ export class CardSearchPagedComponent extends BlazeComponent { runGlobalSearch(queryParams) { this.searching.set(true); + this.debug.set(new QueryDebug()); this.stopSubscription(); this.subscriptionHandle = this.getSubscription(queryParams); } diff --git a/config/const.js b/config/const.js index a275ffa6b..8bbc7d13e 100644 --- a/config/const.js +++ b/config/const.js @@ -49,6 +49,14 @@ export const TYPE_LINKED_BOARD = 'cardType-linkedBoard'; export const TYPE_LINKED_CARD = 'cardType-linkedCard'; export const TYPE_TEMPLATE_BOARD = 'template-board'; export const TYPE_TEMPLATE_CONTAINER = 'template-container'; +export const TYPE_TEMPLATE_CARD = 'template-card'; +export const TYPE_TEMPLATE_LIST = 'template-list'; +export const CARD_TYPES = [ + TYPE_CARD, + TYPE_LINKED_CARD, + TYPE_LINKED_BOARD, + TYPE_TEMPLATE_CARD +]; export const ALLOWED_WAIT_SPINNERS = [ 'Bounce', 'Cube', diff --git a/config/query-classes.js b/config/query-classes.js index 32b0ff58e..a1abec3a1 100644 --- a/config/query-classes.js +++ b/config/query-classes.js @@ -4,6 +4,7 @@ import { OPERATOR_COMMENT, OPERATOR_CREATED_AT, OPERATOR_CREATOR, + OPERATOR_DEBUG, OPERATOR_DUE, OPERATOR_HAS, OPERATOR_LABEL, @@ -11,9 +12,11 @@ import { OPERATOR_LIST, OPERATOR_MEMBER, OPERATOR_MODIFIED_AT, + OPERATOR_ORG, OPERATOR_SORT, OPERATOR_STATUS, OPERATOR_SWIMLANE, + OPERATOR_TEAM, OPERATOR_UNKNOWN, OPERATOR_USER, ORDER_ASCENDING, @@ -34,8 +37,10 @@ import { PREDICATE_OPEN, PREDICATE_OVERDUE, PREDICATE_PRIVATE, + PREDICATE_PROJECTION, PREDICATE_PUBLIC, PREDICATE_QUARTER, + PREDICATE_SELECTOR, PREDICATE_START_AT, PREDICATE_WEEK, PREDICATE_YEAR, @@ -43,6 +48,46 @@ import { import Boards from '../models/boards'; import moment from 'moment'; +export class QueryDebug { + predicate = null; + + constructor(predicate) { + if (predicate) { + this.set(predicate) + } + } + + get() { + return this.predicate; + } + + set(predicate) { + if ([PREDICATE_ALL, PREDICATE_SELECTOR, PREDICATE_PROJECTION].includes( + predicate + )) { + this.predicate = predicate; + } else { + this.predicate = null; + } + } + + show() { + return (this.predicate !== null); + } + + showAll() { + return (this.predicate === PREDICATE_ALL); + } + + showSelector() { + return (this.predicate === PREDICATE_ALL || this.predicate === PREDICATE_SELECTOR); + } + + showProjection() { + return (this.predicate === PREDICATE_ALL || this.predicate === PREDICATE_PROJECTION); + } +} + export class QueryParams { text = ''; @@ -71,11 +116,14 @@ export class QueryParams { } getPredicate(operator) { - if (typeof this.params[operator] === 'object') { - return this.params[operator][0]; - } else { - return this.params[operator]; + if (this.hasOperator(operator)){ + if (typeof this.params[operator] === 'object') { + return this.params[operator][0]; + } else { + return this.params[operator]; + } } + return null; } getPredicates(operator) { @@ -115,6 +163,8 @@ export class QueryErrors { [OPERATOR_ASSIGNEE, 'user-username-not-found'], [OPERATOR_MEMBER, 'user-username-not-found'], [OPERATOR_CREATOR, 'user-username-not-found'], + [OPERATOR_ORG, 'org-name-not-found'], + [OPERATOR_TEAM, 'team-name-not-found'], ]; constructor() { @@ -196,6 +246,10 @@ export class Query { return this._errors.errors(); } + addError(operator, error) { + this._errors.addError(operator, error) + } + errorMessages() { return this._errors.errorMessages(); } @@ -213,6 +267,8 @@ export class Query { } buildParams(queryText) { + this.queryParams = new QueryParams(); + queryText = queryText.trim(); // eslint-disable-next-line no-console //console.log('query:', query); @@ -260,42 +316,51 @@ export class Query { 'operator-has': OPERATOR_HAS, 'operator-sort': OPERATOR_SORT, 'operator-limit': OPERATOR_LIMIT, + 'operator-debug': OPERATOR_DEBUG, + 'operator-org': OPERATOR_ORG, + 'operator-team': OPERATOR_TEAM, }; 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, - }, }; + predicates[OPERATOR_DUE] = { + 'predicate-overdue': PREDICATE_OVERDUE, + }; + predicates[OPERATOR_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, + }; + predicates[OPERATOR_SORT] = { + 'predicate-due': PREDICATE_DUE_AT, + 'predicate-created': PREDICATE_CREATED_AT, + 'predicate-modified': PREDICATE_MODIFIED_AT, + }; + predicates[OPERATOR_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, + }; + predicates[OPERATOR_DEBUG] = { + 'predicate-all': PREDICATE_ALL, + 'predicate-selector': PREDICATE_SELECTOR, + 'predicate-projection': PREDICATE_PROJECTION, + }; + const predicateTranslations = {}; Object.entries(predicates).forEach(([category, catPreds]) => { predicateTranslations[category] = {}; @@ -403,7 +468,7 @@ export class Query { value: moment().format('YYYY-MM-DD'), }; } else { - this.errors.addError(OPERATOR_DUE, { + this.addError(OPERATOR_DUE, { tag: 'operator-number-expected', value: { operator: op, value }, }); @@ -431,27 +496,27 @@ export class Query { value = m.groups.operator; negated = true; } - if (!predicateTranslations.sorts[value]) { - this.errors.addError(OPERATOR_SORT, { + if (!predicateTranslations[OPERATOR_SORT][value]) { + this.addError(OPERATOR_SORT, { tag: 'operator-sort-invalid', value, }); continue; } else { value = { - name: predicateTranslations.sorts[value], + name: predicateTranslations[OPERATOR_SORT][value], order: negated ? ORDER_DESCENDING : ORDER_ASCENDING, }; } } else if (operator === OPERATOR_STATUS) { - if (!predicateTranslations.status[value]) { - this.errors.addError(OPERATOR_STATUS, { + if (!predicateTranslations[OPERATOR_STATUS][value]) { + this.addError(OPERATOR_STATUS, { tag: 'operator-status-invalid', value, }); continue; } else { - value = predicateTranslations.status[value]; + value = predicateTranslations[OPERATOR_STATUS][value]; } } else if (operator === OPERATOR_HAS) { let negated = false; @@ -460,22 +525,22 @@ export class Query { value = m.groups.operator; negated = true; } - if (!predicateTranslations.has[value]) { - this.errors.addError(OPERATOR_HAS, { + if (!predicateTranslations[OPERATOR_HAS][value]) { + this.addError(OPERATOR_HAS, { tag: 'operator-has-invalid', value, }); continue; } else { value = { - field: predicateTranslations.has[value], + field: predicateTranslations[OPERATOR_HAS][value], exists: !negated, }; } } else if (operator === OPERATOR_LIMIT) { const limit = parseInt(value, 10); if (isNaN(limit) || limit < 1) { - this.errors.addError(OPERATOR_LIMIT, { + this.addError(OPERATOR_LIMIT, { tag: 'operator-limit-invalid', value, }); @@ -483,11 +548,21 @@ export class Query { } else { value = limit; } + } else if (operator === OPERATOR_DEBUG) { + if (!predicateTranslations[OPERATOR_DEBUG][value]) { + this.addError(OPERATOR_DEBUG, { + tag: 'operator-debug-invalid', + value, + }); + continue; + } else { + value = predicateTranslations[OPERATOR_DEBUG][value]; + } } this.queryParams.addPredicate(operator, value); } else { - this.errors.addError(OPERATOR_UNKNOWN, { + this.addError(OPERATOR_UNKNOWN, { tag: 'operator-unknown-error', value: op, }); @@ -509,11 +584,13 @@ export class Query { } } - // 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); + if (this.queryParams.hasOperator(OPERATOR_DEBUG)) { + // eslint-disable-next-line no-console + console.log('text:', this.queryParams.text); + console.log('queryParams:', this.queryParams); + } } } diff --git a/config/search-const.js b/config/search-const.js index 5a7f54b6b..19ff1ac9e 100644 --- a/config/search-const.js +++ b/config/search-const.js @@ -3,6 +3,7 @@ export const OPERATOR_ASSIGNEE = 'assignees'; export const OPERATOR_COMMENT = 'comment'; export const OPERATOR_CREATED_AT = 'createdAt'; export const OPERATOR_CREATOR = 'userId'; +export const OPERATOR_DEBUG = 'debug'; export const OPERATOR_DUE = 'dueAt'; export const OPERATOR_BOARD = 'board'; export const OPERATOR_HAS = 'has'; @@ -11,9 +12,11 @@ export const OPERATOR_LIMIT = 'limit'; export const OPERATOR_LIST = 'list'; export const OPERATOR_MEMBER = 'members'; export const OPERATOR_MODIFIED_AT = 'modifiedAt'; +export const OPERATOR_ORG = 'org'; export const OPERATOR_SORT = 'sort'; export const OPERATOR_STATUS = 'status'; export const OPERATOR_SWIMLANE = 'swimlane'; +export const OPERATOR_TEAM = 'team'; export const OPERATOR_UNKNOWN = 'unknown'; export const OPERATOR_USER = 'user'; export const ORDER_ASCENDING = 'asc'; @@ -33,9 +36,11 @@ export const PREDICATE_MODIFIED_AT = 'modifiedAt'; export const PREDICATE_MONTH = 'month'; export const PREDICATE_OPEN = 'open'; export const PREDICATE_OVERDUE = 'overdue'; +export const PREDICATE_PROJECTION = 'projection'; export const PREDICATE_PRIVATE = 'private'; export const PREDICATE_PUBLIC = 'public'; export const PREDICATE_QUARTER = 'quarter'; +export const PREDICATE_SELECTOR = 'selector'; export const PREDICATE_START_AT = 'startAt'; export const PREDICATE_SYSTEM = 'system'; export const PREDICATE_WEEK = 'week'; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 0a0eb7b27..700947c7b 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -943,6 +943,8 @@ "label-color-not-found": "Label color %s not found.", "user-username-not-found": "Username '%s' not found.", "comment-not-found": "Card with comment containing text '%s' not found.", + "org-name-not-found": "Organization '%s' not found.", + "team-name-not-found": "Team '%s' not found.", "globalSearch-title": "Search All Boards", "no-cards-found": "No Cards Found", "one-card-found": "One Card Found", @@ -971,6 +973,9 @@ "operator-comment": "comment", "operator-has": "has", "operator-limit": "limit", + "operator-debug": "debug", + "operator-org": "org", + "operator-team": "team", "predicate-archived": "archived", "predicate-open": "open", "predicate-ended": "ended", @@ -992,12 +997,15 @@ "predicate-member": "member", "predicate-public": "public", "predicate-private": "private", + "predicate-selector": "selector", + "predicate-projection": "projection", "operator-unknown-error": "%s is not an operator", "operator-number-expected": "operator __operator__ expected a number, got '__value__'", "operator-sort-invalid": "sort of '%s' is invalid", "operator-status-invalid": "'%s' is not a valid status", "operator-has-invalid": "%s is not a valid existence check", "operator-limit-invalid": "%s is not a valid limit. Limit should be a positive integer.", + "operator-debug-invalid": "%s is not a valid debug predicate", "next-page": "Next Page", "previous-page": "Previous Page", "heading-notes": "Notes", @@ -1061,6 +1069,8 @@ "orphanedFilesReportTitle": "Orphaned Files Report", "reports": "Reports", "rulesReportTitle": "Rules Report", + "boardsReportTitle": "Boards Report", + "cardsReportTitle": "Cards Report", "copy-swimlane": "Copy Swimlane", "copySwimlanePopup-title": "Copy Swimlane", "display-card-creator": "Display Card Creator", diff --git a/models/boards.js b/models/boards.js index 1195b5891..75d6d20d2 100644 --- a/models/boards.js +++ b/models/boards.js @@ -5,6 +5,7 @@ import { TYPE_TEMPLATE_BOARD, TYPE_TEMPLATE_CONTAINER, } from '/config/const'; +import Users from "./users"; const escapeForRegex = require('escape-string-regexp'); Boards = new Mongo.Collection('boards'); @@ -1479,7 +1480,17 @@ Boards.userSearch = ( return Boards.find(selector, projection); }; -Boards.userBoards = (userId, archived = false, selector = {}) => { +Boards.userBoards = ( + userId, + archived = false, + selector = {}, + projection = {}, +) => { + const user = Users.findOne(userId); + if (!user) { + return []; + } + if (typeof archived === 'boolean') { selector.archived = archived; } @@ -1487,15 +1498,20 @@ Boards.userBoards = (userId, archived = false, selector = {}) => { selector.type = 'board'; } - selector.$or = [{ permission: 'public' }]; - if (userId) { - selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } }); - } - return Boards.find(selector); + selector.$or = [ + { permission: 'public' }, + { members: { $elemMatch: { userId, isActive: true } } }, + { orgs: { $elemMatch: { orgId: { $in: user.orgIds() }, isActive: true } } }, + { teams: { $elemMatch: { teamId: { $in: user.teamIds() }, isActive: true } } }, + ]; + + return Boards.find(selector, projection); }; Boards.userBoardIds = (userId, archived = false, selector = {}) => { - return Boards.userBoards(userId, archived, selector).map(board => { + return Boards.userBoards(userId, archived, selector, { + fields: { _id: 1 }, + }).map(board => { return board._id; }); }; diff --git a/models/lists.js b/models/lists.js index 1e9868d5d..8fe336e79 100644 --- a/models/lists.js +++ b/models/lists.js @@ -345,6 +345,17 @@ Lists.mutations({ }, }); +Lists.userArchivedLists = userId => { + return Lists.find({ + boardId: { $in: Boards.userBoardIds(userId, null) }, + archived: true, + }) +}; + +Lists.userArchivedListIds = () => { + return Lists.userArchivedLists().map(list => { return list._id; }); +}; + Lists.archivedLists = () => { return Lists.find({ archived: true }); }; diff --git a/models/swimlanes.js b/models/swimlanes.js index c83d8139e..1378876c8 100644 --- a/models/swimlanes.js +++ b/models/swimlanes.js @@ -306,6 +306,17 @@ Swimlanes.mutations({ }, }); +Swimlanes.userArchivedSwimlanes = userId => { + return Swimlanes.find({ + boardId: { $in: Boards.userBoardIds(userId, null) }, + archived: true, + }) +}; + +Swimlanes.userArchivedSwimlaneIds = () => { + return Swimlanes.userArchivedSwimlanes().map(swim => { return swim._id; }); +}; + Swimlanes.archivedSwimlanes = () => { return Swimlanes.find({ archived: true }); }; diff --git a/models/users.js b/models/users.js index 70a5b141b..0592b61fc 100644 --- a/models/users.js +++ b/models/users.js @@ -519,6 +519,20 @@ Users.helpers({ } return ''; }, + teamIds() { + if (this.teams) { + // TODO: Should the Team collection be queried to determine if the team isActive? + return this.teams.map(team => { return team.teamId }); + } + return []; + }, + orgIds() { + if (this.orgs) { + // TODO: Should the Org collection be queried to determine if the organization isActive? + return this.orgs.map(org => { return org.orgId }); + } + return []; + }, orgsUserBelongs() { if (this.orgs) { return this.orgs.map(function(org){return org.orgDisplayName}).sort().join(','); @@ -544,32 +558,16 @@ Users.helpers({ return ''; }, boards() { - return Boards.find( - { - 'members.userId': this._id, - }, - { - sort: { - sort: 1 /* boards default sorting */, - }, - }, - ); + return Boards.userBoards(this._id, null, {}, { sort: { sort: 1 } }) }, starredBoards() { const { starredBoards = [] } = this.profile || {}; - return Boards.find( - { - archived: false, - _id: { - $in: starredBoards, - }, - }, - { - sort: { - sort: 1 /* boards default sorting */, - }, - }, + return Boards.userBoards( + this._id, + false, + { _id: { $in: starredBoards } }, + { sort: { sort: 1 } } ); }, @@ -580,18 +578,11 @@ Users.helpers({ invitedBoards() { const { invitedBoards = [] } = this.profile || {}; - return Boards.find( - { - archived: false, - _id: { - $in: invitedBoards, - }, - }, - { - sort: { - sort: 1 /* boards default sorting */, - }, - }, + return Boards.userBoards( + this._id, + false, + { _id: { $in: invitedBoards } }, + { sort: { sort: 1 } } ); }, diff --git a/models/usersessiondata.js b/models/usersessiondata.js index 7d7189ca1..a100574e6 100644 --- a/models/usersessiondata.js +++ b/models/usersessiondata.js @@ -77,6 +77,10 @@ SessionData.attachSchema( optional: true, defaultValue: [], }, + debug: { + type: String, + optional: true, + }, 'errors.$': { type: new SimpleSchema({ tag: { @@ -177,7 +181,7 @@ function unpickleObject(obj) { SessionData.pickle = value => { return JSON.stringify(value, (key, value) => { return pickleValue(value); - }); + }, 2); }; function pickleValue(value) { diff --git a/server/publications/boards.js b/server/publications/boards.js index 23306e087..ef47e52bb 100644 --- a/server/publications/boards.js +++ b/server/publications/boards.js @@ -2,40 +2,47 @@ // non-archived boards: // 1. that the user is a member of // 2. the user has starred +import Users from "../../models/users"; +import Org from "../../models/org"; +import Team from "../../models/team"; + Meteor.publish('boards', function() { const userId = this.userId; // Ensure that the user is connected. If it is not, we need to return an empty // array to tell the client to remove the previously published docs. - if (!Match.test(userId, String) || !userId) return []; + if (!Match.test(userId, String) || !userId) { + return []; + } // Defensive programming to verify that starredBoards has the expected // format -- since the field is in the `profile` a user can modify it. - const { starredBoards = [] } = (Users.findOne(userId) || {}).profile || {}; - check(starredBoards, [String]); + // const { starredBoards = [] } = (Users.findOne(userId) || {}).profile || {}; + // check(starredBoards, [String]); - let currUser = Users.findOne(userId); - let orgIdsUserBelongs = currUser!== 'undefined' && currUser.teams !== 'undefined' ? currUser.orgIdsUserBelongs() : ''; - let teamIdsUserBelongs = currUser!== 'undefined' && currUser.teams !== 'undefined' ? currUser.teamIdsUserBelongs() : ''; - let orgsIds = []; - let teamsIds = []; - if(orgIdsUserBelongs && orgIdsUserBelongs != ''){ - orgsIds = orgIdsUserBelongs.split(','); - } - if(teamIdsUserBelongs && teamIdsUserBelongs != ''){ - teamsIds = teamIdsUserBelongs.split(','); - } + // let currUser = Users.findOne(userId); + // let orgIdsUserBelongs = currUser!== 'undefined' && currUser.teams !== 'undefined' ? currUser.orgIdsUserBelongs() : ''; + // let teamIdsUserBelongs = currUser!== 'undefined' && currUser.teams !== 'undefined' ? currUser.teamIdsUserBelongs() : ''; + // let orgsIds = []; + // let teamsIds = []; + // if(orgIdsUserBelongs && orgIdsUserBelongs != ''){ + // orgsIds = orgIdsUserBelongs.split(','); + // } + // if(teamIdsUserBelongs && teamIdsUserBelongs != ''){ + // teamsIds = teamIdsUserBelongs.split(','); + // } return Boards.find( { archived: false, - $or: [ - { - // _id: { $in: starredBoards }, // Commented out, to get a list of all public boards - permission: 'public', - }, - { members: { $elemMatch: { userId, isActive: true } } }, - {'orgs.orgId': {$in : orgsIds}}, - {'teams.teamId': {$in : teamsIds}}, - ], + _id: { $in: Boards.userBoardIds(userId, false) }, + // $or: [ + // { + // // _id: { $in: starredBoards }, // Commented out, to get a list of all public boards + // permission: 'public', + // }, + // { members: { $elemMatch: { userId, isActive: true } } }, + // {'orgs.orgId': {$in : orgsIds}}, + // {'teams.teamId': {$in : teamsIds}}, + // ], }, { fields: { @@ -58,19 +65,79 @@ Meteor.publish('boards', function() { ); }); +Meteor.publish('boardsReport', function() { + const userId = this.userId; + // Ensure that the user is connected. If it is not, we need to return an empty + // array to tell the client to remove the previously published docs. + if (!Match.test(userId, String) || !userId) return []; + + const boards = Boards.find( + { + _id: { $in: Boards.userBoardIds(userId, null) }, + }, + { + fields: { + _id: 1, + boardId: 1, + archived: 1, + slug: 1, + title: 1, + description: 1, + color: 1, + members: 1, + orgs: 1, + teams: 1, + permission: 1, + type: 1, + sort: 1, + }, + sort: { sort: 1 /* boards default sorting */ }, + }, + ); + + const userIds = []; + const orgIds = []; + const teamIds = []; + boards.forEach(board => { + if (board.members) { + board.members.forEach(member => { + userIds.push(member.userId); + }); + } + if (board.orgs) { + board.orgs.forEach(org => { + orgIds.push(org.orgId); + }); + } + if (board.teams) { + board.teams.forEach(team => { + teamIds.push(team.teamId); + }); + } + }) + + return [ + boards, + Users.find({ _id: { $in: userIds } }, { fields: Users.safeFields }), + Team.find({ _id: { $in: teamIds } }), + Org.find({ _id: { $in: orgIds } }), + ] +}); + Meteor.publish('archivedBoards', function() { const userId = this.userId; if (!Match.test(userId, String)) return []; return Boards.find( { - archived: true, - members: { - $elemMatch: { - userId, - isAdmin: true, - }, - }, + _id: { $in: Boards.userBoardIds(userId, true)}, + // archived: true, + // members: { + // $elemMatch: { + // userId, + // isAdmin: true, + // }, + // }, }, { fields: { diff --git a/server/publications/cards.js b/server/publications/cards.js index 8322de09d..456da3b67 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -17,16 +17,17 @@ import { OPERATOR_COMMENT, OPERATOR_CREATED_AT, OPERATOR_CREATOR, + OPERATOR_DEBUG, OPERATOR_DUE, OPERATOR_HAS, OPERATOR_LABEL, OPERATOR_LIMIT, OPERATOR_LIST, OPERATOR_MEMBER, - OPERATOR_MODIFIED_AT, + OPERATOR_MODIFIED_AT, OPERATOR_ORG, OPERATOR_SORT, OPERATOR_STATUS, - OPERATOR_SWIMLANE, + OPERATOR_SWIMLANE, OPERATOR_TEAM, OPERATOR_USER, ORDER_ASCENDING, PREDICATE_ALL, @@ -47,6 +48,9 @@ import { PREDICATE_SYSTEM, } from '/config/search-const'; import { QueryErrors, QueryParams, Query } from '/config/query-classes'; +import { CARD_TYPES } from '../../config/const'; +import Org from "../../models/org"; +import Team from "../../models/team"; const escapeForRegex = require('escape-string-regexp'); @@ -149,6 +153,51 @@ function buildSelector(queryParams) { } }); } + + if (queryParams.hasOperator(OPERATOR_ORG)) { + const orgs = []; + queryParams.getPredicates(OPERATOR_ORG).forEach(name => { + const org = Org.findOne({ + $or: [ + { orgDisplayName: name }, + { orgShortName: name } + ] + }); + if (org) { + orgs.push(org._id); + } else { + errors.addNotFound(OPERATOR_ORG, name); + } + }); + if (orgs.length) { + boardsSelector.orgs = { + $elemMatch: { orgId: { $in: orgs }, isActive: true } + }; + } + } + + if (queryParams.hasOperator(OPERATOR_TEAM)) { + const teams = []; + queryParams.getPredicates(OPERATOR_TEAM).forEach(name => { + const team = Team.findOne({ + $or: [ + { teamDisplayName: name }, + { teamShortName: name } + ] + }); + if (team) { + teams.push(team._id); + } else { + errors.addNotFound(OPERATOR_TEAM, name); + } + }); + if (teams.length) { + boardsSelector.teams = { + $elemMatch: { teamId: { $in: teams }, isActive: true } + }; + } + } + selector = { type: 'cardType-card', // boardId: { $in: Boards.userBoardIds(userId) }, @@ -167,8 +216,8 @@ function buildSelector(queryParams) { $in: Boards.userBoardIds(userId, archived, boardsSelector), }, }, - { swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } }, - { listId: { $in: Lists.archivedListIds() } }, + { swimlaneId: { $in: Swimlanes.userArchivedSwimlaneIds(userId) } }, + { listId: { $in: Lists.userArchivedListIds(userId) } }, { archived: true }, ], }); @@ -446,8 +495,8 @@ function buildSelector(queryParams) { { _id: { $in: attachments.map(attach => attach.cardId) } }, { _id: { $in: comments.map(com => com.cardId) } }, ]; - if (queryParams.text == "false" || queryParams.text == "true") { - cardsSelector.push({ customFields: { $elemMatch: { value: queryParams.text == "true" ? true : false } } } ); + if (queryParams.text === "false" || queryParams.text === "true") { + cardsSelector.push({ customFields: { $elemMatch: { value: queryParams.text === "true" } } } ); } selector.$and.push({ $or: cardsSelector }); } @@ -458,7 +507,7 @@ function buildSelector(queryParams) { } // eslint-disable-next-line no-console - //console.log('cards selector:', JSON.stringify(selector, null, 2)); + // console.log('cards selector:', JSON.stringify(selector, null, 2)); const query = new Query(); query.selector = selector; @@ -586,6 +635,7 @@ Meteor.publish('brokenCards', function(sessionId) { { boardId: { $in: [null, ''] } }, { swimlaneId: { $in: [null, ''] } }, { listId: { $in: [null, ''] } }, + { type: { $nin: CARD_TYPES } }, ]; // console.log('brokenCards selector:', query.selector); @@ -634,6 +684,7 @@ function findCards(sessionId, query) { selector: SessionData.pickle(query.selector), projection: SessionData.pickle(query.projection), errors: query.errors(), + debug: query.getQueryParams().getPredicate(OPERATOR_DEBUG) }, };