mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
Merge pull request #3437 from jrsupplee/search
Global Search fixes and updates
This commit is contained in:
commit
7f0da49e0a
10 changed files with 489 additions and 264 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
template(name="resultCard")
|
template(name="resultCard")
|
||||||
.result-card-wrapper
|
.result-card-wrapper
|
||||||
a.minicard-wrapper.card-title(href=card.absoluteUrl)
|
a.minicard-wrapper.card-title(href=absoluteUrl)
|
||||||
+minicard(this)
|
+minicard(this)
|
||||||
//= card.title
|
//= card.title
|
||||||
ul.result-card-context-list
|
ul.result-card-context-list
|
||||||
|
|
|
||||||
|
|
@ -14,52 +14,26 @@ 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
|
||||||
if $eq resultsCount.get 0
|
|
||||||
| {{_ 'no-cards-found' }}
|
|
||||||
else if $eq resultsCount.get 1
|
|
||||||
| {{_ 'one-card-found' }}
|
|
||||||
else if $eq resultsCount.get totalHits.get
|
|
||||||
| {{_ 'n-cards-found' resultsCount.get }}
|
|
||||||
else
|
|
||||||
| {{_ 'n-n-of-n-cards-found' 1 resultsCount.get totalHits.get }}
|
|
||||||
if queryErrors.get
|
|
||||||
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 }}
|
||||||
|
else
|
||||||
|
h1
|
||||||
|
= resultsHeading.get
|
||||||
|
a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
|
||||||
each card in results
|
each card in results
|
||||||
a.minicard-wrapper(href=card.absoluteUrl)
|
|
||||||
+resultCard(card)
|
+resultCard(card)
|
||||||
else
|
else
|
||||||
.global-search-instructions
|
.global-search-instructions
|
||||||
h1 Search Operators
|
|
||||||
+viewer
|
+viewer
|
||||||
= 'Searches can include operators to refine the search. Operators are specified by writing the operator'
|
= searchInstructions
|
||||||
= 'name and value separated by a colon. For example, an operator specification of `list:Blocked` would limit the search'
|
|
||||||
= 'to cards that are contained in a list named *Blocked*. If the value contains spaces or special characters'
|
|
||||||
= 'it must be enclosed in quotation marks (e.g. `list:"To Review"`).\n'
|
|
||||||
= 'Available operators are:\n'
|
|
||||||
= '* `board:title` - cards in boards matching the specified title\n'
|
|
||||||
= '* `list:title` - cards in lists matching the specified title\n'
|
|
||||||
= '* `swimlane:title` - cards in swimlanes matching the specified title\n'
|
|
||||||
= '* `label:color` - cards that have a label matching the given color\n'
|
|
||||||
= '* `label:name` - cards that have a label matching the given name\n'
|
|
||||||
= '* `user:username` - cards where the specified user is a member or assignee\n'
|
|
||||||
= '* `@username` - shorthand for `user:username`\n'
|
|
||||||
= '* `#label` - shorthand for `label:color-or-name`\n'
|
|
||||||
= '## Notes\n'
|
|
||||||
= '* Multiple operators may be specified.\n'
|
|
||||||
= '* Similar operators are *OR*ed together. Cards that match any of the conditions will be returned.\n'
|
|
||||||
= ' `list:Available list:Blocked` would return cards contained in any list named *Blocked* or *Available*.\n'
|
|
||||||
= '* Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned.\n'
|
|
||||||
= '`list:Available label:red` returns only cards in the list *Available* with a *red* label.\n'
|
|
||||||
= '* Text searches are case insensitive.\n'
|
|
||||||
|
|
||||||
template(name="globalSearchViewChangePopup")
|
template(name="globalSearchViewChangePopup")
|
||||||
if currentUser
|
if currentUser
|
||||||
|
|
|
||||||
|
|
@ -36,72 +36,106 @@ 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.searchLink = new ReactiveVar(null);
|
||||||
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')) {
|
||||||
|
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);
|
||||||
|
this.queryErrors = results.errors;
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
// console.log('user:', Meteor.user());
|
// console.log('errors:', this.queryErrors);
|
||||||
// eslint-disable-next-line no-console
|
if (this.errorMessages().length) {
|
||||||
// console.log('user:', Meteor.user().sessionData);
|
this.hasQueryErrors.set(true);
|
||||||
// console.log('errors:', results.errors);
|
return null;
|
||||||
this.totalHits.set(Meteor.user().sessionData.totalHits);
|
}
|
||||||
this.resultsCount.set(results.cards.count());
|
|
||||||
this.queryErrors.set(results.errors);
|
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;
|
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) {
|
||||||
|
this.queryErrors.notFound.boards.forEach(board => {
|
||||||
messages.push({ tag: 'board-title-not-found', value: board });
|
messages.push({ tag: 'board-title-not-found', value: board });
|
||||||
});
|
});
|
||||||
errors.notFound.swimlanes.forEach(swim => {
|
this.queryErrors.notFound.swimlanes.forEach(swim => {
|
||||||
messages.push({ tag: 'swimlane-title-not-found', value: swim });
|
messages.push({ tag: 'swimlane-title-not-found', value: swim });
|
||||||
});
|
});
|
||||||
errors.notFound.lists.forEach(list => {
|
this.queryErrors.notFound.lists.forEach(list => {
|
||||||
messages.push({ tag: 'list-title-not-found', value: list });
|
messages.push({ tag: 'list-title-not-found', value: list });
|
||||||
});
|
});
|
||||||
errors.notFound.users.forEach(user => {
|
this.queryErrors.notFound.labels.forEach(label => {
|
||||||
|
messages.push({ tag: 'label-not-found', value: label });
|
||||||
|
});
|
||||||
|
this.queryErrors.notFound.users.forEach(user => {
|
||||||
messages.push({ tag: 'user-username-not-found', value: user });
|
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;
|
||||||
},
|
},
|
||||||
|
|
||||||
events() {
|
searchAllBoards(query) {
|
||||||
return [
|
this.query.set(query);
|
||||||
{
|
|
||||||
'submit .js-search-query-form'(evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
this.query.set(evt.target.searchQuery.value);
|
|
||||||
this.queryErrors.set(null);
|
|
||||||
|
|
||||||
if (!this.query.get()) {
|
this.resetSearch();
|
||||||
this.searching.set(false);
|
|
||||||
this.hasResults.set(false);
|
if (!query) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.searching.set(true);
|
this.searching.set(true);
|
||||||
this.hasResults.set(false);
|
|
||||||
|
|
||||||
let query = this.query.get();
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
// console.log('query:', query);
|
// console.log('query:', query);
|
||||||
|
|
||||||
|
|
@ -121,6 +155,10 @@ BlazeComponent.extendComponent({
|
||||||
operatorMap[TAPi18n.__('operator-label-abbrev')] = 'labels';
|
operatorMap[TAPi18n.__('operator-label-abbrev')] = 'labels';
|
||||||
operatorMap[TAPi18n.__('operator-user')] = 'users';
|
operatorMap[TAPi18n.__('operator-user')] = 'users';
|
||||||
operatorMap[TAPi18n.__('operator-user-abbrev')] = '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';
|
operatorMap[TAPi18n.__('operator-is')] = 'is';
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|
@ -130,6 +168,8 @@ BlazeComponent.extendComponent({
|
||||||
swimlanes: [],
|
swimlanes: [],
|
||||||
lists: [],
|
lists: [],
|
||||||
users: [],
|
users: [],
|
||||||
|
members: [],
|
||||||
|
assignees: [],
|
||||||
labels: [],
|
labels: [],
|
||||||
is: [],
|
is: [],
|
||||||
};
|
};
|
||||||
|
|
@ -154,6 +194,11 @@ BlazeComponent.extendComponent({
|
||||||
}
|
}
|
||||||
if (op in operatorMap) {
|
if (op in operatorMap) {
|
||||||
params[operatorMap[op]].push(m.groups.value);
|
params[operatorMap[op]].push(m.groups.value);
|
||||||
|
} else {
|
||||||
|
this.parsingErrors.push({
|
||||||
|
tag: 'operator-unknown-error',
|
||||||
|
value: op,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +240,97 @@ BlazeComponent.extendComponent({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getResultsHeading() {
|
||||||
|
if (this.resultsCount === 0) {
|
||||||
|
return TAPi18n.__('no-cards-found');
|
||||||
|
} else if (this.resultsCount === 1) {
|
||||||
|
return TAPi18n.__('one-card-found');
|
||||||
|
} else if (this.resultsCount === this.totalHits) {
|
||||||
|
return TAPi18n.__('n-cards-found', this.resultsCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TAPi18n.__('n-n-of-n-cards-found', {
|
||||||
|
start: 1,
|
||||||
|
end: this.resultsCount,
|
||||||
|
total: this.totalHits,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getSearchHref() {
|
||||||
|
const baseUrl = window.location.href.replace(/([?#].*$|\s*$)/, '');
|
||||||
|
return `${baseUrl}?q=${encodeURIComponent(this.query.get())}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
searchInstructions() {
|
||||||
|
tags = {
|
||||||
|
operator_board: TAPi18n.__('operator-board'),
|
||||||
|
operator_list: TAPi18n.__('operator-list'),
|
||||||
|
operator_swimlane: TAPi18n.__('operator-swimlane'),
|
||||||
|
operator_label: TAPi18n.__('operator-label'),
|
||||||
|
operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
|
||||||
|
operator_user: TAPi18n.__('operator-user'),
|
||||||
|
operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
|
||||||
|
operator_member: TAPi18n.__('operator-member'),
|
||||||
|
operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
|
||||||
|
operator_assignee: TAPi18n.__('operator-assignee'),
|
||||||
|
operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
|
||||||
|
};
|
||||||
|
|
||||||
|
text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`;
|
||||||
|
text += `\n${TAPi18n.__('globalSearch-instructions-description', tags)}`;
|
||||||
|
text += `\n${TAPi18n.__('globalSearch-instructions-operators', tags)}`;
|
||||||
|
text += `\n* ${TAPi18n.__(
|
||||||
|
'globalSearch-instructions-operator-board',
|
||||||
|
tags,
|
||||||
|
)}`;
|
||||||
|
text += `\n* ${TAPi18n.__(
|
||||||
|
'globalSearch-instructions-operator-list',
|
||||||
|
tags,
|
||||||
|
)}`;
|
||||||
|
text += `\n* ${TAPi18n.__(
|
||||||
|
'globalSearch-instructions-operator-swimlane',
|
||||||
|
tags,
|
||||||
|
)}`;
|
||||||
|
text += `\n* ${TAPi18n.__(
|
||||||
|
'globalSearch-instructions-operator-label',
|
||||||
|
tags,
|
||||||
|
)}`;
|
||||||
|
text += `\n* ${TAPi18n.__(
|
||||||
|
'globalSearch-instructions-operator-hash',
|
||||||
|
tags,
|
||||||
|
)}`;
|
||||||
|
text += `\n* ${TAPi18n.__(
|
||||||
|
'globalSearch-instructions-operator-user',
|
||||||
|
tags,
|
||||||
|
)}`;
|
||||||
|
text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-at', tags)}`;
|
||||||
|
text += `\n* ${TAPi18n.__(
|
||||||
|
'globalSearch-instructions-operator-member',
|
||||||
|
tags,
|
||||||
|
)}`;
|
||||||
|
text += `\n* ${TAPi18n.__(
|
||||||
|
'globalSearch-instructions-operator-assignee',
|
||||||
|
tags,
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
text += `\n## ${TAPi18n.__('heading-notes')}`;
|
||||||
|
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`;
|
||||||
|
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`;
|
||||||
|
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3', tags)}`;
|
||||||
|
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-4', tags)}`;
|
||||||
|
text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-5', tags)}`;
|
||||||
|
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
|
||||||
|
events() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'submit .js-search-query-form'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.searchAllBoards(evt.target.searchQuery.value);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
font-style: italic
|
font-style: italic
|
||||||
|
|
||||||
code
|
code
|
||||||
color: white
|
color: black
|
||||||
background-color: grey
|
background-color: lightgrey
|
||||||
padding: 0.1rem !important
|
padding: 0.1rem !important
|
||||||
font-size: 0.7rem !important
|
font-size: 0.7rem !important
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,14 @@ FlowRouter.route('/global-search', {
|
||||||
|
|
||||||
Utils.manageCustomUI();
|
Utils.manageCustomUI();
|
||||||
Utils.manageMatomo();
|
Utils.manageMatomo();
|
||||||
|
DocHead.setTitle(TAPi18n.__('globalSearch-title'));
|
||||||
|
|
||||||
|
if (FlowRouter.getQueryParam('q')) {
|
||||||
|
Session.set(
|
||||||
|
'globalQuery',
|
||||||
|
decodeURIComponent(FlowRouter.getQueryParam('q')),
|
||||||
|
);
|
||||||
|
}
|
||||||
BlazeLayout.render('defaultLayout', {
|
BlazeLayout.render('defaultLayout', {
|
||||||
headerBar: 'globalSearchHeaderBar',
|
headerBar: 'globalSearchHeaderBar',
|
||||||
content: 'globalSearch',
|
content: 'globalSearch',
|
||||||
|
|
|
||||||
|
|
@ -868,12 +868,13 @@
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
"list-title-not-found": "List '%s' not found.",
|
"list-title-not-found": "List '%s' not found.",
|
||||||
|
"label-not-found": "Label '%s' not found.",
|
||||||
"user-username-not-found": "Username '%s' not found.",
|
"user-username-not-found": "Username '%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",
|
||||||
"n-cards-found": "%s Cards Found",
|
"n-cards-found": "%s Cards Found",
|
||||||
"n-n-of-n-cards-found": "%s-%s of %s Cards Found",
|
"n-n-of-n-cards-found": "__start__-__end__ of __total__ Cards Found",
|
||||||
"operator-board": "board",
|
"operator-board": "board",
|
||||||
"operator-board-abbrev": "b",
|
"operator-board-abbrev": "b",
|
||||||
"operator-swimlane": "swimlane",
|
"operator-swimlane": "swimlane",
|
||||||
|
|
@ -884,5 +885,29 @@
|
||||||
"operator-label-abbrev": "#",
|
"operator-label-abbrev": "#",
|
||||||
"operator-user": "user",
|
"operator-user": "user",
|
||||||
"operator-user-abbrev": "@",
|
"operator-user-abbrev": "@",
|
||||||
"operator-is": "is"
|
"operator-member": "member",
|
||||||
|
"operator-member-abbrev": "m",
|
||||||
|
"operator-assignee": "assignee",
|
||||||
|
"operator-assignee-abbrev": "a",
|
||||||
|
"operator-is": "is",
|
||||||
|
"operator-unknown-error": "%s is not an operator",
|
||||||
|
"heading-notes": "Notes",
|
||||||
|
"globalSearch-instructions-heading": "Search Instructions",
|
||||||
|
"globalSearch-instructions-description": "Searches can include operators to refine the search. Operators are specified by writing the operator name and value separated by a colon. For example, an operator specification of `list:Blocked` would limit the search to cards that are contained in a list named *Blocked*. If the value contains spaces or special characters it must be enclosed in quotation marks (e.g. `__operator_list__:\"To Review\"`).",
|
||||||
|
"globalSearch-instructions-operators": "Available operators:",
|
||||||
|
"globalSearch-instructions-operator-board": "`__operator_board__:title` - cards in boards matching the specified title",
|
||||||
|
"globalSearch-instructions-operator-list": "`__operator_list__:title` - cards in lists matching the specified title",
|
||||||
|
"globalSearch-instructions-operator-swimlane": "`__operator_swimlane__:title` - cards in swimlanes matching the specified title",
|
||||||
|
"globalSearch-instructions-operator-label": "`__operator_label__:color` `__operator_label__:name` - cards that have a label matching the given color or name",
|
||||||
|
"globalSearch-instructions-operator-hash": "`__operator_label_abbrev__label` - shorthand for `__operator_label__:label`",
|
||||||
|
"globalSearch-instructions-operator-user": "`__operator_user__:username` - cards where the specified user is a *member* or *assignee*",
|
||||||
|
"globalSearch-instructions-operator-at": "`__operator_user_abbrev__username` - shorthand for `user:username`",
|
||||||
|
"globalSearch-instructions-operator-member": "`__operator_member__:username` - cards where the specified user is a *member*",
|
||||||
|
"globalSearch-instructions-operator-assignee": "`__operator_assignee__:username` - cards where the specified user is an *assignee*",
|
||||||
|
"globalSearch-instructions-notes-1": "Multiple operators may be specified.",
|
||||||
|
"globalSearch-instructions-notes-2": "Similar operators are *OR*ed together. Cards that match any of the conditions will be returned.\n`__operator_list__:Available __operator_list__:Blocked` would return cards contained in any list named *Blocked* or *Available*.",
|
||||||
|
"globalSearch-instructions-notes-3": "Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned.\n`__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.",
|
||||||
|
"globalSearch-instructions-notes-4": "Text searches are case insensitive.",
|
||||||
|
"globalSearch-instructions-notes-5": "Currently archived cards are not searched.",
|
||||||
|
"link-to-search": "Link to this search"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1866,16 +1866,29 @@ 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() {
|
||||||
|
this.notFound = {
|
||||||
boards: [],
|
boards: [],
|
||||||
swimlanes: [],
|
swimlanes: [],
|
||||||
lists: [],
|
lists: [],
|
||||||
labels: [],
|
labels: [],
|
||||||
users: [],
|
users: [],
|
||||||
|
members: [],
|
||||||
|
assignees: [],
|
||||||
is: [],
|
is: [],
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
hasErrors() {
|
||||||
|
for (const prop in this.notFound) {
|
||||||
|
if (this.notFound[prop].length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const selector = {
|
const selector = {
|
||||||
archived: false,
|
archived: false,
|
||||||
|
|
@ -1939,25 +1952,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) {
|
||||||
|
|
@ -2003,7 +2054,7 @@ Cards.globalSearch = queryParams => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
errors.notFound.labels.push({ tag: 'label', value: label });
|
errors.notFound.labels.push(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2011,6 +2062,10 @@ Cards.globalSearch = queryParams => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errors.hasErrors()) {
|
||||||
|
return { cards: null, errors };
|
||||||
|
}
|
||||||
|
|
||||||
if (queryParams.text) {
|
if (queryParams.text) {
|
||||||
const regex = new RegExp(queryParams.text, 'i');
|
const regex = new RegExp(queryParams.text, 'i');
|
||||||
|
|
||||||
|
|
@ -2045,14 +2100,6 @@ Cards.globalSearch = queryParams => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
// console.log('count:', cards.count());
|
// console.log('count:', cards.count());
|
||||||
|
|
||||||
if (Meteor.isServer) {
|
|
||||||
Users.update(userId, {
|
|
||||||
$set: {
|
|
||||||
'sessionData.totalHits': cards.count(),
|
|
||||||
'sessionData.lastHit': cards.count() > 50 ? 50 : cards.count(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { cards, errors };
|
return { cards, errors };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -377,6 +377,14 @@ Users.initEasySearch(searchInFields, {
|
||||||
returnFields: [...searchInFields, 'profile.avatarUrl'],
|
returnFields: [...searchInFields, 'profile.avatarUrl'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Users.safeFields = {
|
||||||
|
_id: 1,
|
||||||
|
username: 1,
|
||||||
|
'profile.fullname': 1,
|
||||||
|
'profile.avatarUrl': 1,
|
||||||
|
'profile.initials': 1,
|
||||||
|
};
|
||||||
|
|
||||||
if (Meteor.isClient) {
|
if (Meteor.isClient) {
|
||||||
Users.helpers({
|
Users.helpers({
|
||||||
isBoardMember() {
|
isBoardMember() {
|
||||||
|
|
|
||||||
73
models/usersessiondata.js
Normal file
73
models/usersessiondata.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
SessionData = new Mongo.Collection('sessiondata');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A UserSessionData in Wekan. Organization in Trello.
|
||||||
|
*/
|
||||||
|
SessionData.attachSchema(
|
||||||
|
new SimpleSchema({
|
||||||
|
_id: {
|
||||||
|
/**
|
||||||
|
* the organization id
|
||||||
|
*/
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
autoValue() {
|
||||||
|
if (this.isInsert && !this.isSet) {
|
||||||
|
return incrementCounter('counters', 'orgId', 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
/**
|
||||||
|
* userId of the user
|
||||||
|
*/
|
||||||
|
type: String,
|
||||||
|
optional: false,
|
||||||
|
},
|
||||||
|
totalHits: {
|
||||||
|
/**
|
||||||
|
* total number of hits in the last report query
|
||||||
|
*/
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
lastHit: {
|
||||||
|
/**
|
||||||
|
* the last hit returned from a report query
|
||||||
|
*/
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
/**
|
||||||
|
* creation date of the team
|
||||||
|
*/
|
||||||
|
type: Date,
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
autoValue() {
|
||||||
|
if (this.isInsert) {
|
||||||
|
return new Date();
|
||||||
|
} else if (this.isUpsert) {
|
||||||
|
return { $setOnInsert: new Date() };
|
||||||
|
} else {
|
||||||
|
this.unset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modifiedAt: {
|
||||||
|
type: Date,
|
||||||
|
denyUpdate: false,
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
autoValue() {
|
||||||
|
if (this.isInsert || this.isUpsert || this.isUpdate) {
|
||||||
|
return new Date();
|
||||||
|
} else {
|
||||||
|
this.unset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SessionData;
|
||||||
|
|
@ -72,18 +72,7 @@ Meteor.publish('myCards', function() {
|
||||||
Boards.find({ _id: { $in: boards } }),
|
Boards.find({ _id: { $in: boards } }),
|
||||||
Swimlanes.find({ _id: { $in: swimlanes } }),
|
Swimlanes.find({ _id: { $in: swimlanes } }),
|
||||||
Lists.find({ _id: { $in: lists } }),
|
Lists.find({ _id: { $in: lists } }),
|
||||||
Users.find(
|
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
|
||||||
{ _id: { $in: users } },
|
|
||||||
{
|
|
||||||
fields: {
|
|
||||||
_id: 1,
|
|
||||||
username: 1,
|
|
||||||
'profile.fullname': 1,
|
|
||||||
'profile.avatarUrl': 1,
|
|
||||||
'profile.initials': 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -93,18 +82,7 @@ Meteor.publish('dueCards', function(allUsers = false) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
// console.log('all users:', allUsers);
|
// console.log('all users:', allUsers);
|
||||||
|
|
||||||
const user = Users.findOne(
|
const user = Users.findOne({ _id: this.userId });
|
||||||
{ _id: this.userId },
|
|
||||||
{
|
|
||||||
fields: {
|
|
||||||
_id: 1,
|
|
||||||
username: 1,
|
|
||||||
'profile.fullname': 1,
|
|
||||||
'profile.avatarUrl': 1,
|
|
||||||
'profile.initials': 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const archivedBoards = [];
|
const archivedBoards = [];
|
||||||
Boards.find({ archived: true }).forEach(board => {
|
Boards.find({ archived: true }).forEach(board => {
|
||||||
|
|
@ -115,14 +93,12 @@ Meteor.publish('dueCards', function(allUsers = false) {
|
||||||
let selector = {
|
let selector = {
|
||||||
archived: false,
|
archived: false,
|
||||||
};
|
};
|
||||||
// for admins and users, allow her to see cards only from boards where
|
|
||||||
// she is a member
|
|
||||||
//if (!user.isAdmin) {
|
|
||||||
selector.$or = [
|
selector.$or = [
|
||||||
{ permission: 'public' },
|
{ permission: 'public' },
|
||||||
{ members: { $elemMatch: { userId: user._id, isActive: true } } },
|
{ members: { $elemMatch: { userId: user._id, isActive: true } } },
|
||||||
];
|
];
|
||||||
//}
|
|
||||||
Boards.find(selector).forEach(board => {
|
Boards.find(selector).forEach(board => {
|
||||||
permiitedBoards.push(board._id);
|
permiitedBoards.push(board._id);
|
||||||
});
|
});
|
||||||
|
|
@ -193,18 +169,7 @@ Meteor.publish('dueCards', function(allUsers = false) {
|
||||||
Boards.find({ _id: { $in: boards } }),
|
Boards.find({ _id: { $in: boards } }),
|
||||||
Swimlanes.find({ _id: { $in: swimlanes } }),
|
Swimlanes.find({ _id: { $in: swimlanes } }),
|
||||||
Lists.find({ _id: { $in: lists } }),
|
Lists.find({ _id: { $in: lists } }),
|
||||||
Users.find(
|
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
|
||||||
{ _id: { $in: users } },
|
|
||||||
{
|
|
||||||
fields: {
|
|
||||||
_id: 1,
|
|
||||||
username: 1,
|
|
||||||
'profile.fullname': 1,
|
|
||||||
'profile.avatarUrl': 1,
|
|
||||||
'profile.initials': 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -216,6 +181,20 @@ Meteor.publish('globalSearch', function(queryParams) {
|
||||||
|
|
||||||
const cards = Cards.globalSearch(queryParams).cards;
|
const cards = Cards.globalSearch(queryParams).cards;
|
||||||
|
|
||||||
|
if (!cards) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionData.upsert(
|
||||||
|
{ userId: this.userId },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
totalHits: cards.count(),
|
||||||
|
lastHit: cards.count() > 50 ? 50 : cards.count(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const boards = [];
|
const boards = [];
|
||||||
const swimlanes = [];
|
const swimlanes = [];
|
||||||
const lists = [];
|
const lists = [];
|
||||||
|
|
@ -244,34 +223,21 @@ Meteor.publish('globalSearch', function(queryParams) {
|
||||||
Boards.find({ _id: { $in: boards } }),
|
Boards.find({ _id: { $in: boards } }),
|
||||||
Swimlanes.find({ _id: { $in: swimlanes } }),
|
Swimlanes.find({ _id: { $in: swimlanes } }),
|
||||||
Lists.find({ _id: { $in: lists } }),
|
Lists.find({ _id: { $in: lists } }),
|
||||||
Users.find({ _id: { $in: users } }),
|
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
|
||||||
|
SessionData.find({ userId: this.userId }),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
Meteor.publish('brokenCards', function() {
|
Meteor.publish('brokenCards', function() {
|
||||||
const user = Users.findOne(
|
const user = Users.findOne({ _id: this.userId });
|
||||||
{ _id: this.userId },
|
|
||||||
{
|
|
||||||
fields: {
|
|
||||||
_id: 1,
|
|
||||||
username: 1,
|
|
||||||
'profile.fullname': 1,
|
|
||||||
'profile.avatarUrl': 1,
|
|
||||||
'profile.initials': 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const permiitedBoards = [null];
|
const permiitedBoards = [null];
|
||||||
let selector = {};
|
let selector = {};
|
||||||
// for admins and users, if user is not an admin allow her to see cards only from boards where
|
|
||||||
// she is a member
|
|
||||||
//if (!user.isAdmin) {
|
|
||||||
selector.$or = [
|
selector.$or = [
|
||||||
{ permission: 'public' },
|
{ permission: 'public' },
|
||||||
{ members: { $elemMatch: { userId: user._id, isActive: true } } },
|
{ members: { $elemMatch: { userId: user._id, isActive: true } } },
|
||||||
];
|
];
|
||||||
//}
|
|
||||||
Boards.find(selector).forEach(board => {
|
Boards.find(selector).forEach(board => {
|
||||||
permiitedBoards.push(board._id);
|
permiitedBoards.push(board._id);
|
||||||
});
|
});
|
||||||
|
|
@ -328,17 +294,6 @@ Meteor.publish('brokenCards', function() {
|
||||||
Boards.find({ _id: { $in: boards } }),
|
Boards.find({ _id: { $in: boards } }),
|
||||||
Swimlanes.find({ _id: { $in: swimlanes } }),
|
Swimlanes.find({ _id: { $in: swimlanes } }),
|
||||||
Lists.find({ _id: { $in: lists } }),
|
Lists.find({ _id: { $in: lists } }),
|
||||||
Users.find(
|
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
|
||||||
{ _id: { $in: users } },
|
|
||||||
{
|
|
||||||
fields: {
|
|
||||||
_id: 1,
|
|
||||||
username: 1,
|
|
||||||
'profile.fullname': 1,
|
|
||||||
'profile.avatarUrl': 1,
|
|
||||||
'profile.initials': 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue