Merge pull request #3492 from jrsupplee/new-search

Global Search Update
This commit is contained in:
Lauri Ojansivu 2021-01-27 19:11:21 +02:00 committed by GitHub
commit c10f32cc30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1384 additions and 811 deletions

View file

@ -5,19 +5,28 @@ template(name="resultCard")
//= card.title //= card.title
ul.result-card-context-list ul.result-card-context-list
li.result-card-context(title="{{_ 'board'}}") li.result-card-context(title="{{_ 'board'}}")
+viewer .result-card-block-wrapper
= getBoard.title +viewer
= getBoard.title
if getBoard.archived
i.fa.fa-archive
li.result-card-context.result-card-context-separator li.result-card-context.result-card-context-separator
= ' ' = ' '
| {{_ 'context-separator'}} | {{_ 'context-separator'}}
= ' ' = ' '
li.result-card-context(title="{{_ 'swimlane'}}") li.result-card-context(title="{{_ 'swimlane'}}")
+viewer .result-card-block-wrapper
= getSwimlane.title +viewer
= getSwimlane.title
if getSwimlane.archived
i.fa.fa-archive
li.result-card-context.result-card-context-separator li.result-card-context.result-card-context-separator
= ' ' = ' '
| {{_ 'context-separator'}} | {{_ 'context-separator'}}
= ' ' = ' '
li.result-card-context(title="{{_ 'list'}}") li.result-card-context(title="{{_ 'list'}}")
+viewer .result-card-block-wrapper
= getList.title +viewer
= getList.title
if getList.archived
i.fa.fa-archive

View file

@ -19,3 +19,6 @@
.result-card-context-list .result-card-context-list
margin-bottom: 0.7rem margin-bottom: 0.7rem
.result-card-block-wrapper
display: inline-block

View file

@ -30,13 +30,23 @@ template(name="globalSearch")
div div
each msg in errorMessages each msg in errorMessages
span.global-search-error-messages span.global-search-error-messages
| {{_ msg.tag msg.value }} = msg
else else
h1 h1
= resultsHeading.get = resultsHeading.get
a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}") a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
each card in results each card in results.get
+resultCard(card) +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' }}
else else
.global-search-instructions .global-search-instructions
h2 {{_ 'boards' }} h2 {{_ 'boards' }}

View file

@ -45,19 +45,16 @@ BlazeComponent.extendComponent({
this.myLists = new ReactiveVar([]); this.myLists = new ReactiveVar([]);
this.myLabelNames = new ReactiveVar([]); this.myLabelNames = new ReactiveVar([]);
this.myBoardNames = new ReactiveVar([]); this.myBoardNames = new ReactiveVar([]);
this.results = new ReactiveVar([]);
this.hasNextPage = new ReactiveVar(false);
this.hasPreviousPage = new ReactiveVar(false);
this.queryParams = null; this.queryParams = null;
this.parsingErrors = []; this.parsingErrors = [];
this.resultsCount = 0; this.resultsCount = 0;
this.totalHits = 0; this.totalHits = 0;
this.queryErrors = null; this.queryErrors = null;
this.colorMap = null; this.colorMap = null;
// this.colorMap = {}; this.resultsPerPage = 25;
// for (const color of Boards.simpleSchema()._schema['labels.$.color']
// .allowedValues) {
// this.colorMap[TAPi18n.__(`color-${color}`)] = color;
// }
// // eslint-disable-next-line no-console
// console.log('colorMap:', this.colorMap);
Meteor.call('myLists', (err, data) => { Meteor.call('myLists', (err, data) => {
if (!err) { if (!err) {
@ -80,6 +77,15 @@ BlazeComponent.extendComponent({
onRendered() { onRendered() {
Meteor.subscribe('setting'); Meteor.subscribe('setting');
this.colorMap = {};
for (const color of Boards.simpleSchema()._schema['labels.$.color']
.allowedValues) {
this.colorMap[TAPi18n.__(`color-${color}`)] = color;
}
// // eslint-disable-next-line no-console
// console.log('colorMap:', this.colorMap);
if (Session.get('globalQuery')) { if (Session.get('globalQuery')) {
this.searchAllBoards(Session.get('globalQuery')); this.searchAllBoards(Session.get('globalQuery'));
} }
@ -87,6 +93,7 @@ BlazeComponent.extendComponent({
resetSearch() { resetSearch() {
this.searching.set(false); this.searching.set(false);
this.results.set([]);
this.hasResults.set(false); this.hasResults.set(false);
this.hasQueryErrors.set(false); this.hasQueryErrors.set(false);
this.resultsHeading.set(''); this.resultsHeading.set('');
@ -96,79 +103,83 @@ BlazeComponent.extendComponent({
this.queryErrors = null; this.queryErrors = null;
}, },
results() { getSessionData() {
return SessionData.findOne({
userId: Meteor.userId(),
sessionId: SessionData.getSessionId(),
});
},
getResults() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('getting results'); // console.log('getting results');
if (this.queryParams) { if (this.queryParams) {
const results = Cards.globalSearch(this.queryParams); const sessionData = this.getSessionData();
this.queryErrors = results.errors;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('errors:', this.queryErrors); console.log('selector:', sessionData.getSelector());
if (this.errorMessages().length) { // console.log('session data:', sessionData);
const cards = Cards.find({ _id: { $in: sessionData.cards } });
this.queryErrors = sessionData.errors;
if (this.queryErrors.length) {
this.hasQueryErrors.set(true); this.hasQueryErrors.set(true);
return null; return null;
} }
if (results.cards) { if (cards) {
const sessionData = SessionData.findOne({ userId: Meteor.userId() });
this.totalHits = sessionData.totalHits; this.totalHits = sessionData.totalHits;
this.resultsCount = results.cards.count(); this.resultsCount = cards.count();
this.resultsStart = sessionData.lastHit - this.resultsCount + 1;
this.resultsEnd = sessionData.lastHit;
this.resultsHeading.set(this.getResultsHeading()); this.resultsHeading.set(this.getResultsHeading());
return results.cards; this.results.set(cards);
this.hasNextPage.set(sessionData.lastHit < sessionData.totalHits);
this.hasPreviousPage.set(
sessionData.lastHit - sessionData.resultsCount > 0,
);
} }
} }
this.resultsCount = 0; this.resultsCount = 0;
return []; return null;
}, },
errorMessages() { errorMessages() {
const messages = []; if (this.parsingErrors.length) {
return this.parsingErrorMessages();
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 => {
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[0],
});
} else {
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 });
});
} }
return this.queryErrorMessages();
},
parsingErrorMessages() {
const messages = [];
if (this.parsingErrors.length) { if (this.parsingErrors.length) {
this.parsingErrors.forEach(err => { this.parsingErrors.forEach(err => {
messages.push(err); messages.push(TAPi18n.__(err.tag, err.value));
}); });
} }
return messages; 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) { searchAllBoards(query) {
query = query.trim(); query = query.trim();
// eslint-disable-next-line no-console
console.log('query:', query);
this.query.set(query); this.query.set(query);
this.resetSearch(); this.resetSearch();
@ -177,23 +188,12 @@ BlazeComponent.extendComponent({
return; return;
} }
// eslint-disable-next-line no-console
// console.log('query:', query);
this.searching.set(true); this.searching.set(true);
if (!this.colorMap) { const reOperator1 = /^((?<operator>[\p{Letter}\p{Mark}]+):|(?<abbrev>[#@]))(?<value>[\p{Letter}\p{Mark}]+)(\s+|$)/iu;
this.colorMap = {}; const reOperator2 = /^((?<operator>[\p{Letter}\p{Mark}]+):|(?<abbrev>[#@]))(?<quote>["']*)(?<value>.*?)\k<quote>(\s+|$)/iu;
for (const color of Boards.simpleSchema()._schema['labels.$.color'] const reText = /^(?<text>\S+)(\s+|$)/u;
.allowedValues) { const reQuotedText = /^(?<quote>["'])(?<text>[\w\p{L}]+)\k<quote>(\s+|$)/u;
this.colorMap[TAPi18n.__(`color-${color}`)] = color;
}
}
const reOperator1 = /^((?<operator>\w+):|(?<abbrev>[#@]))(?<value>\w+)(\s+|$)/;
const reOperator2 = /^((?<operator>\w+):|(?<abbrev>[#@]))(?<quote>["']*)(?<value>.*?)\k<quote>(\s+|$)/;
const reText = /^(?<text>\S+)(\s+|$)/;
const reQuotedText = /^(?<quote>["'])(?<text>\w+)\k<quote>(\s+|$)/;
const operators = { const operators = {
'operator-board': 'boards', 'operator-board': 'boards',
@ -210,20 +210,53 @@ BlazeComponent.extendComponent({
'operator-member-abbrev': 'members', 'operator-member-abbrev': 'members',
'operator-assignee': 'assignees', 'operator-assignee': 'assignees',
'operator-assignee-abbrev': 'assignees', 'operator-assignee-abbrev': 'assignees',
'operator-is': 'is', 'operator-status': 'status',
'operator-due': 'dueAt', 'operator-due': 'dueAt',
'operator-created': 'createdAt', 'operator-created': 'createdAt',
'operator-modified': 'modifiedAt', 'operator-modified': 'modifiedAt',
'operator-comment': 'comments',
}; };
const operatorMap = {}; const predicates = {
for (const op in operators) { due: {
operatorMap[TAPi18n.__(op).toLowerCase()] = operators[op]; 'predicate-overdue': 'overdue',
} },
durations: {
'predicate-week': 'week',
'predicate-month': 'month',
'predicate-quarter': 'quarter',
'predicate-year': 'year',
},
status: {
'predicate-archived': 'archived',
'predicate-all': 'all',
'predicate-ended': 'ended',
},
sorts: {
'predicate-due': 'dueAt',
'predicate-created': 'createdAt',
'predicate-modified': 'modifiedAt',
},
};
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 // eslint-disable-next-line no-console
console.log('operatorMap:', operatorMap); // 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 = { const params = {
limit: this.resultsPerPage,
boards: [], boards: [],
swimlanes: [], swimlanes: [],
lists: [], lists: [],
@ -231,10 +264,11 @@ BlazeComponent.extendComponent({
members: [], members: [],
assignees: [], assignees: [],
labels: [], labels: [],
is: [], status: [],
dueAt: null, dueAt: null,
createdAt: null, createdAt: null,
modifiedAt: null, modifiedAt: null,
comments: [],
}; };
let text = ''; let text = '';
@ -255,48 +289,73 @@ BlazeComponent.extendComponent({
} else { } else {
op = m.groups.abbrev; op = m.groups.abbrev;
} }
if (op !== '__proto__') { if (operatorMap.hasOwnProperty(op)) {
if (op in operatorMap) { let value = m.groups.value;
let value = m.groups.value; if (operatorMap[op] === 'labels') {
if (operatorMap[op] === 'labels') { if (value in this.colorMap) {
if (value in this.colorMap) { value = this.colorMap[value];
value = this.colorMap[value]; }
} } else if (
} else if ( ['dueAt', 'createdAt', 'modifiedAt'].includes(operatorMap[op])
['dueAt', 'createdAt', 'modifiedAt'].includes(operatorMap[op]) ) {
) { let days = parseInt(value, 10);
const days = parseInt(value, 10); let duration = null;
if (isNaN(days)) { if (isNaN(days)) {
if ( if (predicateTranslations.durations[value]) {
['day', 'week', 'month', 'quarter', 'year'].includes(value) duration = predicateTranslations.durations[value];
) { value = moment();
value = moment() } else if (predicateTranslations.due[value] === 'overdue') {
.subtract(1, value) value = moment();
.format(); duration = 'days';
} else { days = 0;
this.parsingErrors.push({
tag: 'operator-number-expected',
value: { operator: op, value },
});
value = null;
}
} else { } else {
value = moment() this.parsingErrors.push({
.subtract(days, 'days') tag: 'operator-number-expected',
value: { operator: op, value },
});
value = null;
}
} else {
value = moment();
}
if (value) {
if (operatorMap[op] === 'dueAt') {
value = value.add(days, duration ? duration : 'days').format();
} else {
value = value
.subtract(days, duration ? duration : 'days')
.format(); .format();
} }
} }
if (Array.isArray(params[operatorMap[op]])) { } else if (operatorMap[op] === 'sort') {
params[operatorMap[op]].push(value); if (!predicateTranslations.sorts[value]) {
this.parsingErrors.push({
tag: 'operator-sort-invalid',
value,
});
} else { } else {
params[operatorMap[op]] = value; value = predicateTranslations.sorts[value];
}
} else if (operatorMap[op] === 'status') {
if (!predicateTranslations.status[value]) {
this.parsingErrors.push({
tag: 'operator-status-invalid',
value,
});
} else {
value = predicateTranslations.status[value];
} }
} else {
this.parsingErrors.push({
tag: 'operator-unknown-error',
value: op,
});
} }
if (Array.isArray(params[operatorMap[op]])) {
params[operatorMap[op]].push(value);
} else {
params[operatorMap[op]] = value;
}
} else {
this.parsingErrors.push({
tag: 'operator-unknown-error',
value: op,
});
} }
continue; continue;
} }
@ -324,11 +383,79 @@ BlazeComponent.extendComponent({
this.queryParams = params; this.queryParams = params;
if (this.parsingErrors.length) {
this.searching.set(false);
this.queryErrors = this.parsingErrorMessages();
this.hasResults.set(true);
this.hasQueryErrors.set(true);
return;
}
this.autorun(() => { this.autorun(() => {
const handle = subManager.subscribe('globalSearch', params); const handle = Meteor.subscribe(
'globalSearch',
SessionData.getSessionId(),
params,
);
Tracker.nonreactive(() => { Tracker.nonreactive(() => {
Tracker.autorun(() => { Tracker.autorun(() => {
if (handle.ready()) { if (handle.ready()) {
this.getResults();
this.searching.set(false);
this.hasResults.set(true);
}
});
});
});
},
nextPage() {
sessionData = this.getSessionData();
const params = {
limit: this.resultsPerPage,
selector: sessionData.getSelector(),
skip: sessionData.lastHit,
};
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);
}
});
});
});
},
previousPage() {
sessionData = this.getSessionData();
const params = {
limit: this.resultsPerPage,
selector: sessionData.getSelector(),
skip:
sessionData.lastHit - sessionData.resultsCount - this.resultsPerPage,
};
this.autorun(() => {
const handle = Meteor.subscribe(
'globalSearch',
SessionData.getSessionId(),
params,
);
Tracker.nonreactive(() => {
Tracker.autorun(() => {
if (handle.ready()) {
this.getResults();
this.searching.set(false); this.searching.set(false);
this.hasResults.set(true); this.hasResults.set(true);
} }
@ -347,8 +474,8 @@ BlazeComponent.extendComponent({
} }
return TAPi18n.__('n-n-of-n-cards-found', { return TAPi18n.__('n-n-of-n-cards-found', {
start: 1, start: this.resultsStart,
end: this.resultsCount, end: this.resultsEnd,
total: this.totalHits, total: this.totalHits,
}); });
}, },
@ -363,6 +490,7 @@ BlazeComponent.extendComponent({
operator_board: TAPi18n.__('operator-board'), operator_board: TAPi18n.__('operator-board'),
operator_list: TAPi18n.__('operator-list'), operator_list: TAPi18n.__('operator-list'),
operator_swimlane: TAPi18n.__('operator-swimlane'), operator_swimlane: TAPi18n.__('operator-swimlane'),
operator_comment: TAPi18n.__('operator-comment'),
operator_label: TAPi18n.__('operator-label'), operator_label: TAPi18n.__('operator-label'),
operator_label_abbrev: TAPi18n.__('operator-label-abbrev'), operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
operator_user: TAPi18n.__('operator-user'), operator_user: TAPi18n.__('operator-user'),
@ -371,6 +499,18 @@ BlazeComponent.extendComponent({
operator_member_abbrev: TAPi18n.__('operator-member-abbrev'), operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
operator_assignee: TAPi18n.__('operator-assignee'), operator_assignee: TAPi18n.__('operator-assignee'),
operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'), operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
operator_due: TAPi18n.__('operator-due'),
operator_created: TAPi18n.__('operator-created'),
operator_modified: TAPi18n.__('operator-modified'),
operator_status: TAPi18n.__('operator-status'),
predicate_overdue: TAPi18n.__('predicate-overdue'),
predicate_archived: TAPi18n.__('predicate-archived'),
predicate_all: TAPi18n.__('predicate-all'),
predicate_ended: TAPi18n.__('predicate-ended'),
predicate_week: TAPi18n.__('predicate-week'),
predicate_month: TAPi18n.__('predicate-month'),
predicate_quarter: TAPi18n.__('predicate-quarter'),
predicate_year: TAPi18n.__('predicate-year'),
}; };
text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`;
@ -388,6 +528,10 @@ BlazeComponent.extendComponent({
'globalSearch-instructions-operator-swimlane', 'globalSearch-instructions-operator-swimlane',
tags, tags,
)}`; )}`;
text += `\n* ${TAPi18n.__(
'globalSearch-instructions-operator-comment',
tags,
)}`;
text += `\n* ${TAPi18n.__( text += `\n* ${TAPi18n.__(
'globalSearch-instructions-operator-label', 'globalSearch-instructions-operator-label',
tags, tags,
@ -409,11 +553,27 @@ BlazeComponent.extendComponent({
'globalSearch-instructions-operator-assignee', 'globalSearch-instructions-operator-assignee',
tags, tags,
)}`; )}`;
text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-due', tags)}`;
text += `\n* ${TAPi18n.__(
'globalSearch-instructions-operator-created',
tags,
)}`;
text += `\n* ${TAPi18n.__(
'globalSearch-instructions-operator-modified',
tags,
)}`;
text += `\n* ${TAPi18n.__(
'globalSearch-instructions-status-archived',
tags,
)}`;
text += `\n* ${TAPi18n.__('globalSearch-instructions-status-all', tags)}`;
text += `\n* ${TAPi18n.__('globalSearch-instructions-status-ended', tags)}`;
text += `\n## ${TAPi18n.__('heading-notes')}`; text += `\n## ${TAPi18n.__('heading-notes')}`;
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`;
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`;
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3', tags)}`;
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3-2', tags)}`;
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-4', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-4', tags)}`;
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-5', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-5', tags)}`;
@ -435,10 +595,19 @@ BlazeComponent.extendComponent({
evt.preventDefault(); evt.preventDefault();
this.searchAllBoards(evt.target.searchQuery.value); 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) { 'click .js-label-color'(evt) {
evt.preventDefault(); evt.preventDefault();
const input = document.getElementById('global-search-input');
this.query.set( this.query.set(
`${this.query.get()} ${TAPi18n.__('operator-label')}:"${ `${input.value} ${TAPi18n.__('operator-label')}:"${
evt.currentTarget.textContent evt.currentTarget.textContent
}"`, }"`,
); );
@ -446,8 +615,9 @@ BlazeComponent.extendComponent({
}, },
'click .js-board-title'(evt) { 'click .js-board-title'(evt) {
evt.preventDefault(); evt.preventDefault();
const input = document.getElementById('global-search-input');
this.query.set( this.query.set(
`${this.query.get()} ${TAPi18n.__('operator-board')}:"${ `${input.value} ${TAPi18n.__('operator-board')}:"${
evt.currentTarget.textContent evt.currentTarget.textContent
}"`, }"`,
); );
@ -455,8 +625,9 @@ BlazeComponent.extendComponent({
}, },
'click .js-list-title'(evt) { 'click .js-list-title'(evt) {
evt.preventDefault(); evt.preventDefault();
const input = document.getElementById('global-search-input');
this.query.set( this.query.set(
`${this.query.get()} ${TAPi18n.__('operator-list')}:"${ `${input.value} ${TAPi18n.__('operator-list')}:"${
evt.currentTarget.textContent evt.currentTarget.textContent
}"`, }"`,
); );
@ -464,8 +635,9 @@ BlazeComponent.extendComponent({
}, },
'click .js-label-name'(evt) { 'click .js-label-name'(evt) {
evt.preventDefault(); evt.preventDefault();
const input = document.getElementById('global-search-input');
this.query.set( this.query.set(
`${this.query.get()} ${TAPi18n.__('operator-label')}:"${ `${input.value} ${TAPi18n.__('operator-label')}:"${
evt.currentTarget.textContent evt.currentTarget.textContent
}"`, }"`,
); );

View file

@ -104,3 +104,15 @@ code
.list-title .list-title
background-color: darkgray background-color: darkgray
.global-search-footer
border: none
width: 100%
.global-search-next-page
border: none
text-align: right;
.global-search-previous-page
border: none
text-align: left;

File diff suppressed because it is too large Load diff

View file

@ -876,6 +876,7 @@
"label-not-found": "Label '%s' not found.", "label-not-found": "Label '%s' not found.",
"label-color-not-found": "Label color %s not found.", "label-color-not-found": "Label color %s not found.",
"user-username-not-found": "Username '%s' not found.", "user-username-not-found": "Username '%s' not found.",
"comment-not-found": "Card with comment containing text '%s' not found.",
"globalSearch-title": "Search All Boards", "globalSearch-title": "Search All Boards",
"no-cards-found": "No Cards Found", "no-cards-found": "No Cards Found",
"one-card-found": "One Card Found", "one-card-found": "One Card Found",
@ -895,12 +896,29 @@
"operator-member-abbrev": "m", "operator-member-abbrev": "m",
"operator-assignee": "assignee", "operator-assignee": "assignee",
"operator-assignee-abbrev": "a", "operator-assignee-abbrev": "a",
"operator-is": "is", "operator-status": "status",
"operator-due": "due", "operator-due": "due",
"operator-created": "created", "operator-created": "created",
"operator-modified": "modified", "operator-modified": "modified",
"operator-sort": "sort",
"operator-comment": "comment",
"predicate-archived": "archived",
"predicate-ended": "ended",
"predicate-all": "all",
"predicate-overdue": "overdue",
"predicate-week": "week",
"predicate-month": "month",
"predicate-quarter": "quarter",
"predicate-year": "year",
"predicate-due": "due",
"predicate-modified": "modified",
"predicate-created": "created",
"operator-unknown-error": "%s is not an operator", "operator-unknown-error": "%s is not an operator",
"operator-number-expected": "operator __operator__ expected a number, got '__value__'", "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",
"next-page": "Next Page",
"previous-page": "Previous Page",
"heading-notes": "Notes", "heading-notes": "Notes",
"globalSearch-instructions-heading": "Search Instructions", "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-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\"`).",
@ -908,15 +926,23 @@
"globalSearch-instructions-operator-board": "`__operator_board__:title` - cards in boards matching the specified title", "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-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-swimlane": "`__operator_swimlane__:title` - cards in swimlanes matching the specified title",
"globalSearch-instructions-operator-comment": "`__operator_comment__:text` - cards with with a comment containing *text*.",
"globalSearch-instructions-operator-label": "`__operator_label__:color` `__operator_label__:name` - cards that have a label matching the given color or name", "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-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-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-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-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-operator-assignee": "`__operator_assignee__:username` - cards where the specified user is an *assignee*",
"globalSearch-instructions-operator-due": "`__operator_due__:n` - cards which are due *n* days from now. `__operator_due__:__predicate_overdue__ lists all cards past their due date.",
"globalSearch-instructions-operator-created": "`__operator_created__:n` - cards which which were created *n* days ago",
"globalSearch-instructions-operator-modified": "`__operator_modified__:n` - cards which which were modified *n* days ago",
"globalSearch-instructions-status-archived": "`__operator_status__:__predicate_archived__` - cards that are archived.",
"globalSearch-instructions-status-all": "`__operator_status__:__predicate_all__` - all archived and unarchived cards.",
"globalSearch-instructions-status-ended": "`__operator_status__:__predicate_ended__` - cards with an end date.",
"globalSearch-instructions-notes-1": "Multiple operators may be specified.", "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-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-3": "Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned. `__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.",
"globalSearch-instructions-notes-3-2": "Days can be specified as an integer or using `__predicate_week__`, `__predicate_month__`, `__predicate_quarter__` or `__predicate_year__`",
"globalSearch-instructions-notes-4": "Text searches are case insensitive.", "globalSearch-instructions-notes-4": "Text searches are case insensitive.",
"globalSearch-instructions-notes-5": "Currently archived cards are not searched.", "globalSearch-instructions-notes-5": "Currently archived cards are not searched.",
"link-to-search": "Link to this search", "link-to-search": "Link to this search",

View file

@ -1278,37 +1278,33 @@ Boards.userSearch = (
userId, userId,
selector = {}, selector = {},
projection = {}, projection = {},
includeArchived = false, // includeArchived = false,
) => { ) => {
if (!includeArchived) { // if (!includeArchived) {
selector.archived = false; // selector.archived = false;
} // }
selector.$or = [ selector.$or = [{ permission: 'public' }];
{ permission: 'public' },
{ members: { $elemMatch: { userId, isActive: true } } },
];
if (userId) {
selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } });
}
return Boards.find(selector, projection); return Boards.find(selector, projection);
}; };
Boards.userBoards = (userId, includeArchived = false, selector = {}) => { Boards.userBoards = (userId, archived = false, selector = {}) => {
check(userId, String); if (typeof archived === 'boolean') {
selector.archived = archived;
if (!includeArchived) {
selector = {
archived: false,
};
} }
selector.$or = [ selector.$or = [{ permission: 'public' }];
{ permission: 'public' },
{ members: { $elemMatch: { userId, isActive: true } } },
];
if (userId) {
selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } });
}
return Boards.find(selector); return Boards.find(selector);
}; };
Boards.userBoardIds = (userId, includeArchived = false, selector = {}) => { Boards.userBoardIds = (userId, archived = false, selector = {}) => {
return Boards.userBoards(userId, includeArchived, selector).map(board => { return Boards.userBoards(userId, archived, selector).map(board => {
return board._id; return board._id;
}); });
}; };

View file

@ -1,3 +1,4 @@
const escapeForRegex = require('escape-string-regexp');
CardComments = new Mongo.Collection('card_comments'); CardComments = new Mongo.Collection('card_comments');
/** /**
@ -109,6 +110,28 @@ function commentCreation(userId, doc) {
}); });
} }
CardComments.textSearch = (userId, textArray) => {
const selector = {
boardId: { $in: Boards.userBoardIds(userId) },
$and: [],
};
for (const text of textArray) {
selector.$and.push({ text: new RegExp(escapeForRegex(text)) });
}
// eslint-disable-next-line no-console
// console.log('cardComments selector:', selector);
const comments = CardComments.find(selector);
// eslint-disable-next-line no-console
// console.log('count:', comments.count());
// eslint-disable-next-line no-console
// console.log('cards with comments:', comments.map(com => { return com.cardId }));
return comments;
};
if (Meteor.isServer) { if (Meteor.isServer) {
// Comments are often fetched within a card, so we create an index to make these // Comments are often fetched within a card, so we create an index to make these
// queries more efficient. // queries more efficient.

View file

@ -1863,262 +1863,6 @@ Cards.mutations({
}, },
}); });
Cards.globalSearch = 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: [],
is: [],
};
}
hasErrors() {
for (const prop in this.notFound) {
if (this.notFound[prop].length) {
return true;
}
}
return false;
}
})();
const selector = {
archived: false,
type: 'cardType-card',
boardId: { $in: Boards.userBoardIds(userId) },
swimlaneId: { $nin: Swimlanes.archivedSwimlaneIds() },
listId: { $nin: Lists.archivedListIds() },
};
if (queryParams.boards.length) {
const queryBoards = [];
queryParams.boards.forEach(query => {
const boards = Boards.userSearch(userId, {
title: new RegExp(query, 'i'),
});
if (boards.count()) {
boards.forEach(board => {
queryBoards.push(board._id);
});
} else {
errors.notFound.boards.push(query);
}
});
selector.boardId.$in = queryBoards;
}
if (queryParams.swimlanes.length) {
const querySwimlanes = [];
queryParams.swimlanes.forEach(query => {
const swimlanes = Swimlanes.find({
title: new RegExp(query, 'i'),
});
if (swimlanes.count()) {
swimlanes.forEach(swim => {
querySwimlanes.push(swim._id);
});
} else {
errors.notFound.swimlanes.push(query);
}
});
selector.swimlaneId.$in = querySwimlanes;
}
if (queryParams.lists.length) {
const queryLists = [];
queryParams.lists.forEach(query => {
const lists = Lists.find({
title: new RegExp(query, 'i'),
});
if (lists.count()) {
lists.forEach(list => {
queryLists.push(list._id);
});
} else {
errors.notFound.lists.push(query);
}
});
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) {
queryParams.users.forEach(query => {
const users = Users.find({
username: query,
});
if (users.count()) {
users.forEach(user => {
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: queryMembers } },
{ assignees: { $in: queryAssignees } },
];
} else if (queryMembers.length) {
selector.members = { $in: queryMembers };
} else if (queryAssignees.length) {
selector.assignees = { $in: queryAssignees };
}
if (queryParams.labels.length) {
queryParams.labels.forEach(label => {
const queryLabels = [];
let boards = Boards.userSearch(userId, {
labels: { $elemMatch: { color: label.toLowerCase() } },
});
if (boards.count()) {
boards.forEach(board => {
// eslint-disable-next-line no-console
// console.log('board:', board);
// eslint-disable-next-line no-console
// console.log('board.labels:', board.labels);
board.labels
.filter(boardLabel => {
return boardLabel.color === label.toLowerCase();
})
.forEach(boardLabel => {
queryLabels.push(boardLabel._id);
});
});
} else {
// eslint-disable-next-line no-console
// console.log('label:', label);
const reLabel = new RegExp(label, 'i');
// eslint-disable-next-line no-console
// console.log('reLabel:', reLabel);
boards = Boards.userSearch(userId, {
labels: { $elemMatch: { name: reLabel } },
});
if (boards.count()) {
boards.forEach(board => {
board.labels
.filter(boardLabel => {
return boardLabel.name.match(reLabel);
})
.forEach(boardLabel => {
queryLabels.push(boardLabel._id);
});
});
} else {
errors.notFound.labels.push(label);
}
}
selector.labelIds = { $in: queryLabels };
});
}
if (errors.hasErrors()) {
return { cards: null, errors };
}
if (queryParams.text) {
const regex = new RegExp(queryParams.text, 'i');
selector.$or = [
{ title: regex },
{ description: regex },
{ customFields: { $elemMatch: { value: regex } } },
];
}
// eslint-disable-next-line no-console
// console.log('selector:', selector);
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,
createdAt: 1,
modifiedAt: 1,
labelIds: 1,
},
limit: 50,
});
// eslint-disable-next-line no-console
//console.log('count:', cards.count());
return { cards, errors };
};
//FUNCTIONS FOR creation of Activities //FUNCTIONS FOR creation of Activities
function updateActivities(doc, fieldNames, modifier) { function updateActivities(doc, fieldNames, modifier) {

View file

@ -25,6 +25,13 @@ SessionData.attachSchema(
type: String, type: String,
optional: false, optional: false,
}, },
sessionId: {
/**
* unique session ID
*/
type: String,
optional: false,
},
totalHits: { totalHits: {
/** /**
* total number of hits in the last report query * total number of hits in the last report query
@ -32,6 +39,13 @@ SessionData.attachSchema(
type: Number, type: Number,
optional: true, optional: true,
}, },
resultsCount: {
/**
* number of results returned
*/
type: Number,
optional: true,
},
lastHit: { lastHit: {
/** /**
* the last hit returned from a report query * the last hit returned from a report query
@ -39,6 +53,48 @@ SessionData.attachSchema(
type: Number, type: Number,
optional: true, optional: true,
}, },
cards: {
type: [String],
optional: true,
},
selector: {
type: String,
optional: true,
blackbox: true,
},
errorMessages: {
type: [String],
optional: true,
},
errors: {
type: [Object],
optional: true,
defaultValue: [],
},
'errors.$': {
type: new SimpleSchema({
tag: {
/**
* i18n tag
*/
type: String,
optional: false,
},
value: {
/**
* value for the tag
*/
type: String,
optional: true,
defaultValue: null,
},
color: {
type: Boolean,
optional: true,
defaultValue: false,
},
}),
},
createdAt: { createdAt: {
/** /**
* creation date of the team * creation date of the team
@ -70,4 +126,50 @@ SessionData.attachSchema(
}), }),
); );
SessionData.helpers({
getSelector() {
return SessionData.unpickle(this.selector);
},
});
SessionData.unpickle = pickle => {
return JSON.parse(pickle, (key, value) => {
if (typeof value === 'object') {
if (value.hasOwnProperty('$$class')) {
if (value.$$class === 'RegExp') {
return new RegExp(value.source, value.flags);
}
}
}
return value;
});
};
SessionData.pickle = value => {
return JSON.stringify(value, (key, value) => {
if (typeof value === 'object') {
if (value.constructor.name === 'RegExp') {
return {
$$class: 'RegExp',
source: value.source,
flags: value.flags,
};
}
}
return value;
});
};
if (!Meteor.isServer) {
SessionData.getSessionId = () => {
let sessionId = Session.get('sessionId');
if (!sessionId) {
sessionId = `${String(Meteor.userId())}-${String(Math.random())}`;
Session.set('sessionId', sessionId);
}
return sessionId;
};
}
export default SessionData; export default SessionData;

View file

@ -1,3 +1,5 @@
const escapeForRegex = require('escape-string-regexp');
Meteor.publish('card', cardId => { Meteor.publish('card', cardId => {
check(cardId, String); check(cardId, String);
return Cards.find({ _id: cardId }); return Cards.find({ _id: cardId });
@ -173,59 +175,522 @@ Meteor.publish('dueCards', function(allUsers = false) {
]; ];
}); });
Meteor.publish('globalSearch', function(queryParams) { Meteor.publish('globalSearch', function(sessionId, queryParams) {
check(sessionId, String);
check(queryParams, Object); check(queryParams, Object);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('queryParams:', queryParams); // console.log('queryParams:', queryParams);
const cards = Cards.globalSearch(queryParams).cards; const userId = Meteor.userId();
// eslint-disable-next-line no-console
// console.log('userId:', userId);
if (!cards) { const errors = new (class {
return []; constructor() {
this.notFound = {
boards: [],
swimlanes: [],
lists: [],
labels: [],
users: [],
members: [],
assignees: [],
status: [],
comments: [],
};
this.colorMap = {};
for (const color of Boards.simpleSchema()._schema['labels.$.color']
.allowedValues) {
this.colorMap[TAPi18n.__(`color-${color}`)] = color;
}
}
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: true });
});
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;
}
})();
let selector = {};
let skip = 0;
if (queryParams.skip) {
skip = queryParams.skip;
}
let limit = 25;
if (queryParams.limit) {
limit = queryParams.limit;
} }
SessionData.upsert( if (queryParams.selector) {
{ userId: this.userId }, selector = queryParams.selector;
{ } else {
$set: { let archived = false;
totalHits: cards.count(), let endAt = null;
lastHit: cards.count() > 50 ? 50 : cards.count(), if (queryParams.status.length) {
}, queryParams.status.forEach(status => {
}, if (status === 'archived') {
); archived = true;
} else if (status === 'all') {
const boards = []; archived = null;
const swimlanes = []; } else if (status === 'ended') {
const lists = []; endAt = { $nin: [null, ''] };
const users = [this.userId]; }
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) { selector = {
card.assignees.forEach(userId => { type: 'cardType-card',
users.push(userId); // boardId: { $in: Boards.userBoardIds(userId) },
$and: [],
};
const boardsSelector = {};
if (archived !== null) {
boardsSelector.archived = archived;
if (archived) {
selector.boardId = { $in: Boards.userBoardIds(userId, null) };
selector.$and.push({
$or: [
{ boardId: { $in: Boards.userBoardIds(userId, archived) } },
{ swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } },
{ listId: { $in: Lists.archivedListIds() } },
{ archived: true },
],
});
} else {
selector.boardId = { $in: Boards.userBoardIds(userId, false) };
selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() };
selector.listId = { $nin: Lists.archivedListIds() };
selector.archived = false;
}
} else {
selector.boardId = { $in: Boards.userBoardIds(userId, null) };
}
if (endAt !== null) {
selector.endAt = endAt;
}
if (queryParams.boards.length) {
const queryBoards = [];
queryParams.boards.forEach(query => {
const boards = Boards.userSearch(userId, {
title: new RegExp(escapeForRegex(query), 'i'),
});
if (boards.count()) {
boards.forEach(board => {
queryBoards.push(board._id);
});
} else {
errors.notFound.boards.push(query);
}
});
selector.boardId.$in = queryBoards;
}
if (queryParams.swimlanes.length) {
const querySwimlanes = [];
queryParams.swimlanes.forEach(query => {
const swimlanes = Swimlanes.find({
title: new RegExp(escapeForRegex(query), 'i'),
});
if (swimlanes.count()) {
swimlanes.forEach(swim => {
querySwimlanes.push(swim._id);
});
} else {
errors.notFound.swimlanes.push(query);
}
});
if (!selector.swimlaneId.hasOwnProperty('swimlaneId')) {
selector.swimlaneId = { $in: [] };
}
selector.swimlaneId.$in = querySwimlanes;
}
if (queryParams.lists.length) {
const queryLists = [];
queryParams.lists.forEach(query => {
const lists = Lists.find({
title: new RegExp(escapeForRegex(query), 'i'),
});
if (lists.count()) {
lists.forEach(list => {
queryLists.push(list._id);
});
} else {
errors.notFound.lists.push(query);
}
});
if (!selector.hasOwnProperty('listId')) {
selector.listId = { $in: [] };
}
selector.listId.$in = queryLists;
}
if (queryParams.comments.length) {
const cardIds = CardComments.textSearch(userId, queryParams.comments).map(
com => {
return com.cardId;
},
);
if (cardIds.length) {
selector._id = { $in: cardIds };
} else {
errors.notFound.comments.push(queryParams.comments);
}
}
if (queryParams.dueAt !== null) {
selector.dueAt = { $lte: 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) {
queryParams.users.forEach(query => {
const users = Users.find({
username: query,
});
if (users.count()) {
users.forEach(user => {
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.$and.push({
$or: [
{ 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) {
queryParams.labels.forEach(label => {
const queryLabels = [];
let boards = Boards.userSearch(userId, {
labels: { $elemMatch: { color: label.toLowerCase() } },
});
if (boards.count()) {
boards.forEach(board => {
// eslint-disable-next-line no-console
// console.log('board:', board);
// eslint-disable-next-line no-console
// console.log('board.labels:', board.labels);
board.labels
.filter(boardLabel => {
return boardLabel.color === label.toLowerCase();
})
.forEach(boardLabel => {
queryLabels.push(boardLabel._id);
});
});
} else {
// eslint-disable-next-line no-console
// console.log('label:', label);
const reLabel = new RegExp(escapeForRegex(label), 'i');
// eslint-disable-next-line no-console
// console.log('reLabel:', reLabel);
boards = Boards.userSearch(userId, {
labels: { $elemMatch: { name: reLabel } },
});
if (boards.count()) {
boards.forEach(board => {
board.labels
.filter(boardLabel => {
return boardLabel.name.match(reLabel);
})
.forEach(boardLabel => {
queryLabels.push(boardLabel._id);
});
});
} else {
errors.notFound.labels.push(label);
}
}
selector.labelIds = { $in: queryLabels };
});
}
if (queryParams.text) {
const regex = new RegExp(escapeForRegex(queryParams.text), 'i');
selector.$and.push({
$or: [
{ title: regex },
{ description: regex },
{ customFields: { $elemMatch: { value: regex } } },
{
_id: {
$in: CardComments.textSearch(userId, [queryParams.text]).map(
com => com.cardId,
),
},
},
],
});
}
if (selector.$and.length === 0) {
delete selector.$and;
}
}
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('users:', users); // console.log('selector:', selector);
return [ // eslint-disable-next-line no-console
cards, // console.log('selector.$and:', selector.$and);
Boards.find({ _id: { $in: boards } }),
Swimlanes.find({ _id: { $in: swimlanes } }), let cards = null;
Lists.find({ _id: { $in: lists } }),
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), if (!errors.hasErrors()) {
SessionData.find({ userId: this.userId }), const projection = {
]; 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,
createdAt: 1,
modifiedAt: 1,
labelIds: 1,
customFields: 1,
},
skip,
limit,
};
if (queryParams.sort === 'due') {
projection.sort = {
dueAt: 1,
boardId: 1,
swimlaneId: 1,
listId: 1,
sort: 1,
};
} else if (queryParams.sort === 'modified') {
projection.sort = {
modifiedAt: -1,
boardId: 1,
swimlaneId: 1,
listId: 1,
sort: 1,
};
} else if (queryParams.sort === 'created') {
projection.sort = {
createdAt: -1,
boardId: 1,
swimlaneId: 1,
listId: 1,
sort: 1,
};
} else if (queryParams.sort === 'system') {
projection.sort = {
boardId: 1,
swimlaneId: 1,
listId: 1,
modifiedAt: 1,
sort: 1,
};
}
// eslint-disable-next-line no-console
// console.log('projection:', projection);
cards = Cards.find(selector, projection);
// eslint-disable-next-line no-console
// console.log('count:', cards.count());
}
const update = {
$set: {
totalHits: 0,
lastHit: 0,
resultsCount: 0,
cards: [],
errors: errors.errorMessages(),
selector: SessionData.pickle(selector),
},
};
if (cards) {
update.$set.totalHits = cards.count();
update.$set.lastHit =
skip + limit < cards.count() ? skip + limit : cards.count();
update.$set.cards = cards.map(card => {
return card._id;
});
update.$set.resultsCount = update.$set.cards.length;
}
SessionData.upsert({ userId, sessionId }, update);
// remove old session data
SessionData.remove({
userId,
modifiedAt: {
$lt: new Date(
moment()
.subtract(1, 'day')
.format(),
),
},
});
if (cards) {
const boards = [];
const swimlanes = [];
const lists = [];
const customFieldIds = [];
const users = [this.userId];
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);
});
}
if (card.customFields) {
card.customFields.forEach(field => {
customFieldIds.push(field._id);
});
}
});
const fields = {
_id: 1,
title: 1,
archived: 1,
sort: 1,
type: 1,
};
return [
cards,
Boards.find(
{ _id: { $in: boards } },
{ fields: { ...fields, labels: 1, color: 1 } },
),
Swimlanes.find(
{ _id: { $in: swimlanes } },
{ fields: { ...fields, color: 1 } },
),
Lists.find({ _id: { $in: lists } }, { fields }),
CustomFields.find({ _id: { $in: customFieldIds } }),
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
SessionData.find({ userId: this.userId, sessionId }),
];
}
return [SessionData.find({ userId: this.userId, sessionId })];
}); });
Meteor.publish('brokenCards', function() { Meteor.publish('brokenCards', function() {