mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
Merge pull request #3492 from jrsupplee/new-search
Global Search Update
This commit is contained in:
commit
c10f32cc30
12 changed files with 1384 additions and 811 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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' }}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}"`,
|
}"`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
256
models/cards.js
256
models/cards.js
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue