mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
Global Search improvements
* support for searching from the URL * add support for searching by assignee and member
This commit is contained in:
parent
8059856c39
commit
d74dc92681
7 changed files with 299 additions and 162 deletions
|
|
@ -14,20 +14,21 @@ template(name="globalSearch")
|
||||||
if currentUser
|
if currentUser
|
||||||
.wrapper
|
.wrapper
|
||||||
form.global-search-instructions.js-search-query-form
|
form.global-search-instructions.js-search-query-form
|
||||||
input.global-search-query-input(type="text" name="searchQuery" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
|
input.global-search-query-input(type="text" name="searchQuery" placeholder="{{_ 'search-example'}}" value="{{ query.get }}" autofocus dir="auto")
|
||||||
if searching.get
|
if searching.get
|
||||||
+spinner
|
+spinner
|
||||||
else if hasResults.get
|
else if hasResults.get
|
||||||
.global-search-dueat-list-wrapper
|
.global-search-results-list-wrapper
|
||||||
h1
|
if hasQueryErrors.get
|
||||||
= resultsHeading
|
|
||||||
if queryErrors.get
|
|
||||||
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.tag msg.value }}
|
||||||
each card in results
|
else
|
||||||
+resultCard(card)
|
h1
|
||||||
|
= resultsHeading.get
|
||||||
|
each card in results
|
||||||
|
+resultCard(card)
|
||||||
else
|
else
|
||||||
.global-search-instructions
|
.global-search-instructions
|
||||||
+viewer
|
+viewer
|
||||||
|
|
|
||||||
|
|
@ -36,69 +36,225 @@ BlazeComponent.extendComponent({
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
onCreated() {
|
onCreated() {
|
||||||
this.isPageReady = new ReactiveVar(true);
|
|
||||||
this.searching = new ReactiveVar(false);
|
this.searching = new ReactiveVar(false);
|
||||||
this.hasResults = new ReactiveVar(false);
|
this.hasResults = new ReactiveVar(false);
|
||||||
|
this.hasQueryErrors = new ReactiveVar(false);
|
||||||
this.query = new ReactiveVar('');
|
this.query = new ReactiveVar('');
|
||||||
|
this.resultsHeading = new ReactiveVar('');
|
||||||
this.queryParams = null;
|
this.queryParams = null;
|
||||||
this.resultsCount = new ReactiveVar(0);
|
this.parsingErrors = [];
|
||||||
this.totalHits = new ReactiveVar(0);
|
this.resultsCount = 0;
|
||||||
this.queryErrors = new ReactiveVar(null);
|
this.totalHits = 0;
|
||||||
|
this.queryErrors = null;
|
||||||
Meteor.subscribe('setting');
|
Meteor.subscribe('setting');
|
||||||
|
if (Session.get('globalQuery')) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log(Session.get('globalQuery'));
|
||||||
|
this.searchAllBoards(Session.get('globalQuery'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetSearch() {
|
||||||
|
this.searching.set(false);
|
||||||
|
this.hasResults.set(false);
|
||||||
|
this.hasQueryErrors.set(false);
|
||||||
|
this.resultsHeading.set('');
|
||||||
|
this.parsingErrors = [];
|
||||||
|
this.resultsCount = 0;
|
||||||
|
this.totalHits = 0;
|
||||||
|
this.queryErrors = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
results() {
|
results() {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('getting results');
|
||||||
if (this.queryParams) {
|
if (this.queryParams) {
|
||||||
const results = Cards.globalSearch(this.queryParams);
|
const results = Cards.globalSearch(this.queryParams);
|
||||||
const sessionData = SessionData.findOne({ userId: Meteor.userId() });
|
this.queryErrors = results.errors;
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
// console.log('sessionData:', sessionData);
|
console.log('errors:', this.queryErrors);
|
||||||
// console.log('errors:', results.errors);
|
if (this.errorMessages().length) {
|
||||||
this.totalHits.set(sessionData.totalHits);
|
this.hasQueryErrors.set(true);
|
||||||
this.resultsCount.set(results.cards.count());
|
return null;
|
||||||
this.queryErrors.set(results.errors);
|
}
|
||||||
return results.cards;
|
|
||||||
|
if (results.cards) {
|
||||||
|
const sessionData = SessionData.findOne({ userId: Meteor.userId() });
|
||||||
|
this.totalHits = sessionData.totalHits;
|
||||||
|
this.resultsCount = results.cards.count();
|
||||||
|
this.resultsHeading.set(this.getResultsHeading());
|
||||||
|
return results.cards;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.resultsCount.set(0);
|
this.resultsCount = 0;
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
errorMessages() {
|
errorMessages() {
|
||||||
const errors = this.queryErrors.get();
|
|
||||||
const messages = [];
|
const messages = [];
|
||||||
|
|
||||||
errors.notFound.boards.forEach(board => {
|
if (this.queryErrors) {
|
||||||
messages.push({ tag: 'board-title-not-found', value: board });
|
this.queryErrors.notFound.boards.forEach(board => {
|
||||||
});
|
messages.push({ tag: 'board-title-not-found', value: board });
|
||||||
errors.notFound.swimlanes.forEach(swim => {
|
});
|
||||||
messages.push({ tag: 'swimlane-title-not-found', value: swim });
|
this.queryErrors.notFound.swimlanes.forEach(swim => {
|
||||||
});
|
messages.push({ tag: 'swimlane-title-not-found', value: swim });
|
||||||
errors.notFound.lists.forEach(list => {
|
});
|
||||||
messages.push({ tag: 'list-title-not-found', value: list });
|
this.queryErrors.notFound.lists.forEach(list => {
|
||||||
});
|
messages.push({ tag: 'list-title-not-found', value: list });
|
||||||
errors.notFound.labels.forEach(label => {
|
});
|
||||||
messages.push({ tag: 'label-not-found', value: label });
|
this.queryErrors.notFound.labels.forEach(label => {
|
||||||
});
|
messages.push({ tag: 'label-not-found', value: label });
|
||||||
errors.notFound.users.forEach(user => {
|
});
|
||||||
messages.push({ tag: 'user-username-not-found', value: user });
|
this.queryErrors.notFound.users.forEach(user => {
|
||||||
});
|
messages.push({ tag: 'user-username-not-found', value: user });
|
||||||
|
});
|
||||||
|
this.queryErrors.notFound.members.forEach(user => {
|
||||||
|
messages.push({ tag: 'user-username-not-found', value: user });
|
||||||
|
});
|
||||||
|
this.queryErrors.notFound.assignees.forEach(user => {
|
||||||
|
messages.push({ tag: 'user-username-not-found', value: user });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.parsingErrors.length) {
|
||||||
|
this.parsingErrors.forEach(err => {
|
||||||
|
messages.push(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
},
|
},
|
||||||
|
|
||||||
resultsHeading() {
|
searchAllBoards(query) {
|
||||||
if (this.resultsCount.get() === 0) {
|
this.query.set(query);
|
||||||
|
|
||||||
|
this.resetSearch();
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searching.set(true);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log('query:', query);
|
||||||
|
|
||||||
|
const reOperator1 = /^((?<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 operatorMap = {};
|
||||||
|
operatorMap[TAPi18n.__('operator-board')] = 'boards';
|
||||||
|
operatorMap[TAPi18n.__('operator-board-abbrev')] = 'boards';
|
||||||
|
operatorMap[TAPi18n.__('operator-swimlane')] = 'swimlanes';
|
||||||
|
operatorMap[TAPi18n.__('operator-swimlane-abbrev')] = 'swimlanes';
|
||||||
|
operatorMap[TAPi18n.__('operator-list')] = 'lists';
|
||||||
|
operatorMap[TAPi18n.__('operator-list-abbrev')] = 'lists';
|
||||||
|
operatorMap[TAPi18n.__('operator-label')] = 'labels';
|
||||||
|
operatorMap[TAPi18n.__('operator-label-abbrev')] = 'labels';
|
||||||
|
operatorMap[TAPi18n.__('operator-user')] = 'users';
|
||||||
|
operatorMap[TAPi18n.__('operator-user-abbrev')] = 'users';
|
||||||
|
operatorMap[TAPi18n.__('operator-member')] = 'members';
|
||||||
|
operatorMap[TAPi18n.__('operator-member-abbrev')] = 'members';
|
||||||
|
operatorMap[TAPi18n.__('operator-assignee')] = 'assignees';
|
||||||
|
operatorMap[TAPi18n.__('operator-assignee-abbrev')] = 'assignees';
|
||||||
|
operatorMap[TAPi18n.__('operator-is')] = 'is';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log('operatorMap:', operatorMap);
|
||||||
|
const params = {
|
||||||
|
boards: [],
|
||||||
|
swimlanes: [],
|
||||||
|
lists: [],
|
||||||
|
users: [],
|
||||||
|
members: [],
|
||||||
|
assignees: [],
|
||||||
|
labels: [],
|
||||||
|
is: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
while (query) {
|
||||||
|
m = query.match(reOperator1);
|
||||||
|
if (!m) {
|
||||||
|
m = query.match(reOperator2);
|
||||||
|
if (m) {
|
||||||
|
query = query.replace(reOperator2, '');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query = query.replace(reOperator1, '');
|
||||||
|
}
|
||||||
|
if (m) {
|
||||||
|
let op;
|
||||||
|
if (m.groups.operator) {
|
||||||
|
op = m.groups.operator.toLowerCase();
|
||||||
|
} else {
|
||||||
|
op = m.groups.abbrev;
|
||||||
|
}
|
||||||
|
if (op in operatorMap) {
|
||||||
|
params[operatorMap[op]].push(m.groups.value);
|
||||||
|
} else {
|
||||||
|
this.parsingErrors.push({
|
||||||
|
tag: 'operator-unknown-error',
|
||||||
|
value: op,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
m = query.match(reQuotedText);
|
||||||
|
if (!m) {
|
||||||
|
m = query.match(reText);
|
||||||
|
if (m) {
|
||||||
|
query = query.replace(reText, '');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query = query.replace(reQuotedText, '');
|
||||||
|
}
|
||||||
|
if (m) {
|
||||||
|
text += (text ? ' ' : '') + m.groups.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log('text:', text);
|
||||||
|
params.text = text;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log('params:', params);
|
||||||
|
|
||||||
|
this.queryParams = params;
|
||||||
|
|
||||||
|
this.autorun(() => {
|
||||||
|
const handle = subManager.subscribe('globalSearch', params);
|
||||||
|
Tracker.nonreactive(() => {
|
||||||
|
Tracker.autorun(() => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log('ready:', handle.ready());
|
||||||
|
if (handle.ready()) {
|
||||||
|
this.searching.set(false);
|
||||||
|
this.hasResults.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getResultsHeading() {
|
||||||
|
if (this.resultsCount === 0) {
|
||||||
return TAPi18n.__('no-cards-found');
|
return TAPi18n.__('no-cards-found');
|
||||||
} else if (this.resultsCount.get() === 1) {
|
} else if (this.resultsCount === 1) {
|
||||||
return TAPi18n.__('one-card-found');
|
return TAPi18n.__('one-card-found');
|
||||||
} else if (this.resultsCount.get() === this.totalHits.get()) {
|
} else if (this.resultsCount === this.totalHits) {
|
||||||
return TAPi18n.__('n-cards-found', this.resultsCount.get());
|
return TAPi18n.__('n-cards-found', this.resultsCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TAPi18n.__('n-n-of-n-cards-found', {
|
return TAPi18n.__('n-n-of-n-cards-found', {
|
||||||
start: 1,
|
start: 1,
|
||||||
end: this.resultsCount.get(),
|
end: this.resultsCount,
|
||||||
total: this.totalHits.get(),
|
total: this.totalHits,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -111,6 +267,10 @@ BlazeComponent.extendComponent({
|
||||||
operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
|
operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
|
||||||
operator_user: TAPi18n.__('operator-user'),
|
operator_user: TAPi18n.__('operator-user'),
|
||||||
operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
|
operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
|
||||||
|
operator_member: TAPi18n.__('operator-member'),
|
||||||
|
operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
|
||||||
|
operator_assignee: TAPi18n.__('operator-assignee'),
|
||||||
|
operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
|
||||||
};
|
};
|
||||||
|
|
||||||
text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`;
|
text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`;
|
||||||
|
|
@ -141,6 +301,14 @@ BlazeComponent.extendComponent({
|
||||||
tags,
|
tags,
|
||||||
)}`;
|
)}`;
|
||||||
text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-at', tags)}`;
|
text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-at', tags)}`;
|
||||||
|
text += `\n* ${TAPi18n.__(
|
||||||
|
'globalSearch-instructions-operator-member',
|
||||||
|
tags,
|
||||||
|
)}`;
|
||||||
|
text += `\n* ${TAPi18n.__(
|
||||||
|
'globalSearch-instructions-operator-assignee',
|
||||||
|
tags,
|
||||||
|
)}`;
|
||||||
|
|
||||||
text += `\n## ${TAPi18n.__('heading-notes')}`;
|
text += `\n## ${TAPi18n.__('heading-notes')}`;
|
||||||
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`;
|
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`;
|
||||||
|
|
@ -157,111 +325,7 @@ BlazeComponent.extendComponent({
|
||||||
{
|
{
|
||||||
'submit .js-search-query-form'(evt) {
|
'submit .js-search-query-form'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.query.set(evt.target.searchQuery.value);
|
this.searchAllBoards(evt.target.searchQuery.value);
|
||||||
this.queryErrors.set(null);
|
|
||||||
|
|
||||||
if (!this.query.get()) {
|
|
||||||
this.searching.set(false);
|
|
||||||
this.hasResults.set(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searching.set(true);
|
|
||||||
this.hasResults.set(false);
|
|
||||||
|
|
||||||
let query = this.query.get();
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
// console.log('query:', query);
|
|
||||||
|
|
||||||
const reOperator1 = /^((?<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 operatorMap = {};
|
|
||||||
operatorMap[TAPi18n.__('operator-board')] = 'boards';
|
|
||||||
operatorMap[TAPi18n.__('operator-board-abbrev')] = 'boards';
|
|
||||||
operatorMap[TAPi18n.__('operator-swimlane')] = 'swimlanes';
|
|
||||||
operatorMap[TAPi18n.__('operator-swimlane-abbrev')] = 'swimlanes';
|
|
||||||
operatorMap[TAPi18n.__('operator-list')] = 'lists';
|
|
||||||
operatorMap[TAPi18n.__('operator-list-abbrev')] = 'lists';
|
|
||||||
operatorMap[TAPi18n.__('operator-label')] = 'labels';
|
|
||||||
operatorMap[TAPi18n.__('operator-label-abbrev')] = 'labels';
|
|
||||||
operatorMap[TAPi18n.__('operator-user')] = 'users';
|
|
||||||
operatorMap[TAPi18n.__('operator-user-abbrev')] = 'users';
|
|
||||||
operatorMap[TAPi18n.__('operator-is')] = 'is';
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
// console.log('operatorMap:', operatorMap);
|
|
||||||
const params = {
|
|
||||||
boards: [],
|
|
||||||
swimlanes: [],
|
|
||||||
lists: [],
|
|
||||||
users: [],
|
|
||||||
labels: [],
|
|
||||||
is: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
let text = '';
|
|
||||||
while (query) {
|
|
||||||
m = query.match(reOperator1);
|
|
||||||
if (!m) {
|
|
||||||
m = query.match(reOperator2);
|
|
||||||
if (m) {
|
|
||||||
query = query.replace(reOperator2, '');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query = query.replace(reOperator1, '');
|
|
||||||
}
|
|
||||||
if (m) {
|
|
||||||
let op;
|
|
||||||
if (m.groups.operator) {
|
|
||||||
op = m.groups.operator.toLowerCase();
|
|
||||||
} else {
|
|
||||||
op = m.groups.abbrev;
|
|
||||||
}
|
|
||||||
if (op in operatorMap) {
|
|
||||||
params[operatorMap[op]].push(m.groups.value);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
m = query.match(reQuotedText);
|
|
||||||
if (!m) {
|
|
||||||
m = query.match(reText);
|
|
||||||
if (m) {
|
|
||||||
query = query.replace(reText, '');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query = query.replace(reQuotedText, '');
|
|
||||||
}
|
|
||||||
if (m) {
|
|
||||||
text += (text ? ' ' : '') + m.groups.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
// console.log('text:', text);
|
|
||||||
params.text = text;
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
// console.log('params:', params);
|
|
||||||
|
|
||||||
this.queryParams = params;
|
|
||||||
|
|
||||||
this.autorun(() => {
|
|
||||||
const handle = subManager.subscribe('globalSearch', params);
|
|
||||||
Tracker.nonreactive(() => {
|
|
||||||
Tracker.autorun(() => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
// console.log('ready:', handle.ready());
|
|
||||||
if (handle.ready()) {
|
|
||||||
this.searching.set(false);
|
|
||||||
this.hasResults.set(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
margin-top: 0
|
margin-top: 0
|
||||||
margin-bottom: 10px
|
margin-bottom: 10px
|
||||||
|
|
||||||
.global-search-dueat-list-wrapper
|
.global-search-results-list-wrapper
|
||||||
max-width: 500px
|
max-width: 500px
|
||||||
margin-right: auto
|
margin-right: auto
|
||||||
margin-left: auto
|
margin-left: auto
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,11 @@ FlowRouter.route('/global-search', {
|
||||||
|
|
||||||
Utils.manageCustomUI();
|
Utils.manageCustomUI();
|
||||||
Utils.manageMatomo();
|
Utils.manageMatomo();
|
||||||
|
DocHead.setTitle(TAPi18n.__('globalSearch-title'));
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('URL Params:', FlowRouter.getQueryParam('q'));
|
||||||
|
Session.set('globalQuery', decodeURI(FlowRouter.getQueryParam('q')));
|
||||||
BlazeLayout.render('defaultLayout', {
|
BlazeLayout.render('defaultLayout', {
|
||||||
headerBar: 'globalSearchHeaderBar',
|
headerBar: 'globalSearchHeaderBar',
|
||||||
content: 'globalSearch',
|
content: 'globalSearch',
|
||||||
|
|
|
||||||
|
|
@ -885,7 +885,12 @@
|
||||||
"operator-label-abbrev": "#",
|
"operator-label-abbrev": "#",
|
||||||
"operator-user": "user",
|
"operator-user": "user",
|
||||||
"operator-user-abbrev": "@",
|
"operator-user-abbrev": "@",
|
||||||
|
"operator-member": "member",
|
||||||
|
"operator-member-abbrev": "m",
|
||||||
|
"operator-assignee": "assignee",
|
||||||
|
"operator-assignee-abbrev": "a",
|
||||||
"operator-is": "is",
|
"operator-is": "is",
|
||||||
|
"operator-unknown-error": "%s is not an operator",
|
||||||
"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\"`).",
|
||||||
|
|
@ -897,6 +902,8 @@
|
||||||
"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-assignee": "`__operator_assignee__:username` - cards where the specified user is an *assignee*",
|
||||||
"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.\n`__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.",
|
||||||
|
|
|
||||||
|
|
@ -1735,16 +1735,31 @@ Cards.globalSearch = queryParams => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
// console.log('userId:', userId);
|
// console.log('userId:', userId);
|
||||||
|
|
||||||
const errors = {
|
const errors = new (class {
|
||||||
notFound: {
|
constructor() {
|
||||||
boards: [],
|
this.notFound = {
|
||||||
swimlanes: [],
|
boards: [],
|
||||||
lists: [],
|
swimlanes: [],
|
||||||
labels: [],
|
lists: [],
|
||||||
users: [],
|
labels: [],
|
||||||
is: [],
|
users: [],
|
||||||
},
|
members: [],
|
||||||
};
|
assignees: [],
|
||||||
|
is: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
hasErrors() {
|
||||||
|
for (const prop in this.notFound) {
|
||||||
|
if (this.notFound[prop].length) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('errors in:', prop, this.notFound[prop]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const selector = {
|
const selector = {
|
||||||
archived: false,
|
archived: false,
|
||||||
|
|
@ -1808,25 +1823,63 @@ Cards.globalSearch = queryParams => {
|
||||||
selector.listId.$in = queryLists;
|
selector.listId.$in = queryLists;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryMembers = [];
|
||||||
|
const queryAssignees = [];
|
||||||
if (queryParams.users.length) {
|
if (queryParams.users.length) {
|
||||||
const queryUsers = [];
|
|
||||||
queryParams.users.forEach(query => {
|
queryParams.users.forEach(query => {
|
||||||
const users = Users.find({
|
const users = Users.find({
|
||||||
username: query,
|
username: query,
|
||||||
});
|
});
|
||||||
if (users.count()) {
|
if (users.count()) {
|
||||||
users.forEach(user => {
|
users.forEach(user => {
|
||||||
queryUsers.push(user._id);
|
queryMembers.push(user._id);
|
||||||
|
queryAssignees.push(user._id);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
errors.notFound.users.push(query);
|
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 = [
|
selector.$or = [
|
||||||
{ members: { $in: queryUsers } },
|
{ members: { $in: queryMembers } },
|
||||||
{ assignees: { $in: queryUsers } },
|
{ assignees: { $in: queryAssignees } },
|
||||||
];
|
];
|
||||||
|
} else if (queryMembers.length) {
|
||||||
|
selector.members = { $in: queryMembers };
|
||||||
|
} else if (queryAssignees.length) {
|
||||||
|
selector.assignees = { $in: queryAssignees };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryParams.labels.length) {
|
if (queryParams.labels.length) {
|
||||||
|
|
@ -1880,6 +1933,10 @@ Cards.globalSearch = queryParams => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errors.hasErrors()) {
|
||||||
|
return { cards: null, errors };
|
||||||
|
}
|
||||||
|
|
||||||
if (queryParams.text) {
|
if (queryParams.text) {
|
||||||
const regex = new RegExp(queryParams.text, 'i');
|
const regex = new RegExp(queryParams.text, 'i');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,10 @@ Meteor.publish('globalSearch', function(queryParams) {
|
||||||
|
|
||||||
const cards = Cards.globalSearch(queryParams).cards;
|
const cards = Cards.globalSearch(queryParams).cards;
|
||||||
|
|
||||||
|
if (!cards) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
SessionData.upsert(
|
SessionData.upsert(
|
||||||
{ userId: this.userId },
|
{ userId: this.userId },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue