mirror of
https://github.com/wekan/wekan.git
synced 2025-12-18 08:20:12 +01:00
commit
94cb33a0ce
15 changed files with 806 additions and 2 deletions
23
client/components/cards/resultCard.jade
Normal file
23
client/components/cards/resultCard.jade
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
template(name="resultCard")
|
||||||
|
.result-card-wrapper
|
||||||
|
a.minicard-wrapper.card-title(href=card.absoluteUrl)
|
||||||
|
+minicard(this)
|
||||||
|
//= card.title
|
||||||
|
ul.result-card-context-list
|
||||||
|
li.result-card-context(title="{{_ 'board'}}")
|
||||||
|
+viewer
|
||||||
|
= getBoard.title
|
||||||
|
li.result-card-context.result-card-context-separator
|
||||||
|
= ' '
|
||||||
|
| {{_ 'context-separator'}}
|
||||||
|
= ' '
|
||||||
|
li.result-card-context(title="{{_ 'swimlane'}}")
|
||||||
|
+viewer
|
||||||
|
= getSwimlane.title
|
||||||
|
li.result-card-context.result-card-context-separator
|
||||||
|
= ' '
|
||||||
|
| {{_ 'context-separator'}}
|
||||||
|
= ' '
|
||||||
|
li.result-card-context(title="{{_ 'list'}}")
|
||||||
|
+viewer
|
||||||
|
= getList.title
|
||||||
11
client/components/cards/resultCard.js
Normal file
11
client/components/cards/resultCard.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
Template.resultCard.helpers({
|
||||||
|
userId() {
|
||||||
|
return Meteor.userId();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
events() {
|
||||||
|
return [{}];
|
||||||
|
},
|
||||||
|
}).register('resultCard');
|
||||||
21
client/components/cards/resultCard.styl
Normal file
21
client/components/cards/resultCard.styl
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
.result-card-list-wrapper
|
||||||
|
margin: 1rem
|
||||||
|
border-radius: 5px
|
||||||
|
padding: 1.5rem
|
||||||
|
padding-top: 0.75rem
|
||||||
|
display: inline-block
|
||||||
|
min-width: 250px
|
||||||
|
max-width: 350px
|
||||||
|
|
||||||
|
.result-card-wrapper
|
||||||
|
margin-top: 0
|
||||||
|
margin-bottom: 10px
|
||||||
|
|
||||||
|
.result-card-context
|
||||||
|
display: inline-block
|
||||||
|
|
||||||
|
.result-card-context-separator
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.result-card-context-list
|
||||||
|
margin-bottom: 0.7rem
|
||||||
78
client/components/main/globalSearch.jade
Normal file
78
client/components/main/globalSearch.jade
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
template(name="globalSearchHeaderBar")
|
||||||
|
h1
|
||||||
|
i.fa.fa-search
|
||||||
|
| {{_ 'globalSearch-title'}}
|
||||||
|
|
||||||
|
template(name="globalSearchModalTitle")
|
||||||
|
h2
|
||||||
|
i.fa.fa-keyboard-o
|
||||||
|
| {{_ 'globalSearch-title'}}
|
||||||
|
|
||||||
|
template(name="globalSearch")
|
||||||
|
.wrapper
|
||||||
|
form.global-search-instructions.js-search-query-form
|
||||||
|
input.global-search-query-input(type="text" name="searchQuery" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
|
||||||
|
if searching.get
|
||||||
|
+spinner
|
||||||
|
else if hasResults.get
|
||||||
|
.global-search-dueat-list-wrapper
|
||||||
|
h1
|
||||||
|
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
|
||||||
|
each msg in errorMessages
|
||||||
|
span.global-search-error-messages
|
||||||
|
| {{_ msg.tag msg.value }}
|
||||||
|
each card in results
|
||||||
|
+resultCard(card)
|
||||||
|
else
|
||||||
|
.global-search-instructions
|
||||||
|
h1 Search Operators
|
||||||
|
+viewer
|
||||||
|
= '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. `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")
|
||||||
|
ul.pop-over-list
|
||||||
|
li
|
||||||
|
with "globalSearchViewChange-choice-me"
|
||||||
|
a.js-global-search-view-me
|
||||||
|
i.fa.fa-user.colorful
|
||||||
|
| {{_ 'globalSearchViewChange-choice-me'}}
|
||||||
|
if $eq Utils.globalSearchView "me"
|
||||||
|
i.fa.fa-check
|
||||||
|
li
|
||||||
|
with "globalSearchViewChange-choice-all"
|
||||||
|
a.js-global-search-view-all
|
||||||
|
i.fa.fa-users.colorful
|
||||||
|
| {{_ 'globalSearchViewChange-choice-all'}}
|
||||||
|
span.sub-name
|
||||||
|
+viewer
|
||||||
|
| {{_ 'globalSearchViewChange-choice-all-description' }}
|
||||||
|
if $eq Utils.globalSearchView "all"
|
||||||
|
i.fa.fa-check
|
||||||
201
client/components/main/globalSearch.js
Normal file
201
client/components/main/globalSearch.js
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
const subManager = new SubsManager();
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
events() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'click .js-due-cards-view-change': Popup.open('globalSearchViewChange'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}).register('globalSearchHeaderBar');
|
||||||
|
|
||||||
|
Template.globalSearch.helpers({
|
||||||
|
userId() {
|
||||||
|
return Meteor.userId();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
events() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'click .js-due-cards-view-me'() {
|
||||||
|
Utils.setDueCardsView('me');
|
||||||
|
Popup.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
'click .js-due-cards-view-all'() {
|
||||||
|
Utils.setDueCardsView('all');
|
||||||
|
Popup.close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}).register('globalSearchViewChangePopup');
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
onCreated() {
|
||||||
|
this.isPageReady = new ReactiveVar(true);
|
||||||
|
this.searching = new ReactiveVar(false);
|
||||||
|
this.hasResults = new ReactiveVar(false);
|
||||||
|
this.query = new ReactiveVar('');
|
||||||
|
this.queryParams = null;
|
||||||
|
this.resultsCount = new ReactiveVar(0);
|
||||||
|
this.totalHits = new ReactiveVar(0);
|
||||||
|
this.queryErrors = new ReactiveVar(null);
|
||||||
|
Meteor.subscribe('setting');
|
||||||
|
},
|
||||||
|
|
||||||
|
results() {
|
||||||
|
if (this.queryParams) {
|
||||||
|
const results = Cards.globalSearch(this.queryParams);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log('user:', Meteor.user());
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log('user:', Meteor.user().sessionData);
|
||||||
|
// console.log('errors:', results.errors);
|
||||||
|
this.totalHits.set(Meteor.user().sessionData.totalHits);
|
||||||
|
this.resultsCount.set(results.cards.count());
|
||||||
|
this.queryErrors.set(results.errors);
|
||||||
|
return results.cards;
|
||||||
|
}
|
||||||
|
this.resultsCount.set(0);
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
errorMessages() {
|
||||||
|
const errors = this.queryErrors.get();
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
errors.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 });
|
||||||
|
});
|
||||||
|
errors.notFound.lists.forEach(list => {
|
||||||
|
messages.push({ tag: 'list-title-not-found', value: list });
|
||||||
|
});
|
||||||
|
errors.notFound.users.forEach(user => {
|
||||||
|
messages.push({ tag: 'user-username-not-found', value: user });
|
||||||
|
});
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
},
|
||||||
|
|
||||||
|
events() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'submit .js-search-query-form'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.query.set(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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}).register('globalSearch');
|
||||||
97
client/components/main/globalSearch.styl
Normal file
97
client/components/main/globalSearch.styl
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
.global-search-board-wrapper
|
||||||
|
border-radius: 8px
|
||||||
|
//padding: 0.5rem
|
||||||
|
min-width: 400px
|
||||||
|
border-width: 8px
|
||||||
|
border-color: grey
|
||||||
|
border-style: solid
|
||||||
|
margin-bottom: 2rem
|
||||||
|
margin-right: auto
|
||||||
|
margin-left: auto
|
||||||
|
|
||||||
|
.global-search-board-title
|
||||||
|
font-size: 1.4rem
|
||||||
|
font-weight: bold
|
||||||
|
padding: 0.5rem
|
||||||
|
background-color: grey
|
||||||
|
color: white
|
||||||
|
|
||||||
|
.global-search-swimlane-title
|
||||||
|
font-size: 1.1rem
|
||||||
|
font-weight: bold
|
||||||
|
padding: 0.5rem
|
||||||
|
padding-bottom: 0.4rem
|
||||||
|
margin-top: 0
|
||||||
|
margin-bottom: 0.5rem
|
||||||
|
//border-top: black 1px solid
|
||||||
|
//border-bottom: black 1px solid
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
.swimlane-default-color
|
||||||
|
background-color: lightgrey
|
||||||
|
|
||||||
|
.global-search-list-title
|
||||||
|
font-weight: bold
|
||||||
|
font-size: 1.1rem
|
||||||
|
//padding-bottom: 0
|
||||||
|
//margin-bottom: 0
|
||||||
|
text-align: center
|
||||||
|
margin-bottom: 0.7rem
|
||||||
|
|
||||||
|
.global-search-list-wrapper
|
||||||
|
margin: 1rem
|
||||||
|
border-radius: 5px
|
||||||
|
padding: 1.5rem
|
||||||
|
padding-top: 0.75rem
|
||||||
|
display: inline-block
|
||||||
|
min-width: 250px
|
||||||
|
max-width: 350px
|
||||||
|
|
||||||
|
.global-search-card-wrapper
|
||||||
|
margin-top: 0
|
||||||
|
margin-bottom: 10px
|
||||||
|
|
||||||
|
.global-search-dueat-list-wrapper
|
||||||
|
max-width: 500px
|
||||||
|
margin-right: auto
|
||||||
|
margin-left: auto
|
||||||
|
|
||||||
|
.global-search-field-name
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.global-search-context
|
||||||
|
display: inline-block
|
||||||
|
|
||||||
|
.global-search-context-separator
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.global-search-context-list
|
||||||
|
margin-bottom: 0.7rem
|
||||||
|
|
||||||
|
.global-search-error-messages
|
||||||
|
color: darkred
|
||||||
|
|
||||||
|
.global-search-instructions
|
||||||
|
width: 40%
|
||||||
|
min-width: 400px
|
||||||
|
margin-right: auto
|
||||||
|
margin-left: auto
|
||||||
|
line-height: 150%
|
||||||
|
|
||||||
|
.global-search-query-input
|
||||||
|
width: 90% !important
|
||||||
|
margin-right: auto
|
||||||
|
margin-left: auto
|
||||||
|
|
||||||
|
.global-search-operator
|
||||||
|
font-family: Courier
|
||||||
|
|
||||||
|
.global-search-value
|
||||||
|
font-family: Courier
|
||||||
|
font-style: italic
|
||||||
|
|
||||||
|
code
|
||||||
|
color: white
|
||||||
|
background-color: grey
|
||||||
|
padding: 0.1rem !important
|
||||||
|
font-size: 0.7rem !important
|
||||||
|
|
@ -21,6 +21,10 @@ template(name="memberMenuPopup")
|
||||||
a.js-due-cards(href="{{pathFor 'due-cards'}}")
|
a.js-due-cards(href="{{pathFor 'due-cards'}}")
|
||||||
i.fa.fa-calendar
|
i.fa.fa-calendar
|
||||||
| {{_ 'dueCards-title'}}
|
| {{_ 'dueCards-title'}}
|
||||||
|
li
|
||||||
|
a.js-global-search(href="{{pathFor 'global-search'}}")
|
||||||
|
i.fa.fa-search
|
||||||
|
| {{_ 'globalSearch-title'}}
|
||||||
li
|
li
|
||||||
a.js-broken-cards(href="{{pathFor 'broken-cards'}}")
|
a.js-broken-cards(href="{{pathFor 'broken-cards'}}")
|
||||||
i.fa.fa-chain-broken
|
i.fa.fa-chain-broken
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,23 @@ FlowRouter.route('/due-cards', {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
FlowRouter.route('/global-search', {
|
||||||
|
name: 'global-search',
|
||||||
|
action() {
|
||||||
|
Filter.reset();
|
||||||
|
// EscapeActions.executeAll();
|
||||||
|
EscapeActions.executeUpTo('popup-close');
|
||||||
|
|
||||||
|
Utils.manageCustomUI();
|
||||||
|
Utils.manageMatomo();
|
||||||
|
|
||||||
|
BlazeLayout.render('defaultLayout', {
|
||||||
|
headerBar: 'globalSearchHeaderBar',
|
||||||
|
content: 'globalSearch',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
FlowRouter.route('/broken-cards', {
|
FlowRouter.route('/broken-cards', {
|
||||||
name: 'broken-cards',
|
name: 'broken-cards',
|
||||||
action() {
|
action() {
|
||||||
|
|
@ -165,7 +182,6 @@ FlowRouter.route('/broken-cards', {
|
||||||
headerBar: 'brokenCardsHeaderBar',
|
headerBar: 'brokenCardsHeaderBar',
|
||||||
content: brokenCardsTemplate,
|
content: brokenCardsTemplate,
|
||||||
});
|
});
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -864,5 +864,25 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
"broken-cards": "Broken Cards"
|
"broken-cards": "Broken Cards",
|
||||||
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
"list-title-not-found": "List '%s' not found.",
|
||||||
|
"user-username-not-found": "Username '%s' not found.",
|
||||||
|
"globalSearch-title": "Search All Boards",
|
||||||
|
"no-cards-found": "No Cards Found",
|
||||||
|
"one-card-found": "One Card Found",
|
||||||
|
"n-cards-found": "%s Cards Found",
|
||||||
|
"n-n-of-n-cards-found": "%s-%s of %s Cards Found",
|
||||||
|
"operator-board": "board",
|
||||||
|
"operator-board-abbrev": "b",
|
||||||
|
"operator-swimlane": "swimlane",
|
||||||
|
"operator-swimlane-abbrev": "s",
|
||||||
|
"operator-list": "list",
|
||||||
|
"operator-list-abbrev": "l",
|
||||||
|
"operator-label": "label",
|
||||||
|
"operator-label-abbrev": "#",
|
||||||
|
"operator-user": "user",
|
||||||
|
"operator-user-abbrev": "@",
|
||||||
|
"operator-is": "is"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1208,6 +1208,45 @@ function boardRemover(userId, doc) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Boards.userSearch = (
|
||||||
|
userId,
|
||||||
|
selector = {},
|
||||||
|
projection = {},
|
||||||
|
includeArchived = false,
|
||||||
|
) => {
|
||||||
|
if (!includeArchived) {
|
||||||
|
selector.archived = false;
|
||||||
|
}
|
||||||
|
selector.$or = [
|
||||||
|
{ permission: 'public' },
|
||||||
|
{ members: { $elemMatch: { userId, isActive: true } } },
|
||||||
|
];
|
||||||
|
|
||||||
|
return Boards.find(selector, projection);
|
||||||
|
};
|
||||||
|
|
||||||
|
Boards.userBoards = (userId, includeArchived = false, selector = {}) => {
|
||||||
|
check(userId, String);
|
||||||
|
|
||||||
|
if (!includeArchived) {
|
||||||
|
selector = {
|
||||||
|
archived: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
selector.$or = [
|
||||||
|
{ permission: 'public' },
|
||||||
|
{ members: { $elemMatch: { userId, isActive: true } } },
|
||||||
|
];
|
||||||
|
|
||||||
|
return Boards.find(selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
Boards.userBoardIds = (userId, includeArchived = false, selector = {}) => {
|
||||||
|
return Boards.userBoards(userId, includeArchived, selector).map(board => {
|
||||||
|
return board._id;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
Boards.allow({
|
Boards.allow({
|
||||||
insert: Meteor.userId,
|
insert: Meteor.userId,
|
||||||
|
|
|
||||||
195
models/cards.js
195
models/cards.js
|
|
@ -1730,6 +1730,201 @@ Cards.mutations({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cards.globalSearch = queryParams => {
|
||||||
|
const userId = Meteor.userId();
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log('userId:', userId);
|
||||||
|
|
||||||
|
const errors = {
|
||||||
|
notFound: {
|
||||||
|
boards: [],
|
||||||
|
swimlanes: [],
|
||||||
|
lists: [],
|
||||||
|
labels: [],
|
||||||
|
users: [],
|
||||||
|
is: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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.users.length) {
|
||||||
|
const queryUsers = [];
|
||||||
|
queryParams.users.forEach(query => {
|
||||||
|
const users = Users.find({
|
||||||
|
username: query,
|
||||||
|
});
|
||||||
|
if (users.count()) {
|
||||||
|
users.forEach(user => {
|
||||||
|
queryUsers.push(user._id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
errors.notFound.users.push(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
selector.$or = [
|
||||||
|
{ members: { $in: queryUsers } },
|
||||||
|
{ assignees: { $in: queryUsers } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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({ tag: 'label', value: label });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selector.labelIds = { $in: queryLabels };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
labelIds: 1,
|
||||||
|
},
|
||||||
|
limit: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// 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 };
|
||||||
|
};
|
||||||
|
|
||||||
//FUNCTIONS FOR creation of Activities
|
//FUNCTIONS FOR creation of Activities
|
||||||
|
|
||||||
function updateActivities(doc, fieldNames, modifier) {
|
function updateActivities(doc, fieldNames, modifier) {
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,16 @@ Lists.mutations({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Lists.archivedLists = () => {
|
||||||
|
return Lists.find({ archived: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
Lists.archivedListIds = () => {
|
||||||
|
return Lists.archivedLists().map(list => {
|
||||||
|
return list._id;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
applyWipLimit(listId, limit) {
|
applyWipLimit(listId, limit) {
|
||||||
check(listId, String);
|
check(listId, String);
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,16 @@ Swimlanes.mutations({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Swimlanes.archivedSwimlanes = () => {
|
||||||
|
return Swimlanes.find({ archived: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
Swimlanes.archivedSwimlaneIds = () => {
|
||||||
|
return Swimlanes.archivedSwimlanes().map(swim => {
|
||||||
|
return swim._id;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
Swimlanes.hookOptions.after.update = { fetchPrevious: false };
|
Swimlanes.hookOptions.after.update = { fetchPrevious: false };
|
||||||
|
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
|
|
|
||||||
|
|
@ -311,6 +311,33 @@ Users.attachSchema(
|
||||||
optional: false,
|
optional: false,
|
||||||
defaultValue: 'password',
|
defaultValue: 'password',
|
||||||
},
|
},
|
||||||
|
sessionData: {
|
||||||
|
/**
|
||||||
|
* profile settings
|
||||||
|
*/
|
||||||
|
type: Object,
|
||||||
|
optional: true,
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
autoValue() {
|
||||||
|
if (this.isInsert && !this.isSet) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sessionData.totalHits': {
|
||||||
|
/**
|
||||||
|
* Total hits from last search
|
||||||
|
*/
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'sessionData.lastHit': {
|
||||||
|
/**
|
||||||
|
* last hit that was returned
|
||||||
|
*/
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,46 @@ Meteor.publish('dueCards', function(allUsers = false) {
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Meteor.publish('globalSearch', function(queryParams) {
|
||||||
|
check(queryParams, Object);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log('queryParams:', queryParams);
|
||||||
|
|
||||||
|
const cards = Cards.globalSearch(queryParams).cards;
|
||||||
|
|
||||||
|
const boards = [];
|
||||||
|
const swimlanes = [];
|
||||||
|
const lists = [];
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
// console.log('users:', users);
|
||||||
|
return [
|
||||||
|
cards,
|
||||||
|
Boards.find({ _id: { $in: boards } }),
|
||||||
|
Swimlanes.find({ _id: { $in: swimlanes } }),
|
||||||
|
Lists.find({ _id: { $in: lists } }),
|
||||||
|
Users.find({ _id: { $in: users } }),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
Meteor.publish('brokenCards', function() {
|
Meteor.publish('brokenCards', function() {
|
||||||
const user = Users.findOne(this.userId);
|
const user = Users.findOne(this.userId);
|
||||||
|
|
||||||
|
|
@ -221,11 +261,22 @@ Meteor.publish('brokenCards', function() {
|
||||||
const boards = [];
|
const boards = [];
|
||||||
const swimlanes = [];
|
const swimlanes = [];
|
||||||
const lists = [];
|
const lists = [];
|
||||||
|
const users = [];
|
||||||
|
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
if (card.boardId) boards.push(card.boardId);
|
if (card.boardId) boards.push(card.boardId);
|
||||||
if (card.swimlaneId) swimlanes.push(card.swimlaneId);
|
if (card.swimlaneId) swimlanes.push(card.swimlaneId);
|
||||||
if (card.listId) lists.push(card.listId);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -233,5 +284,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({ _id: { $in: users } }),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue