mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +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'}}")
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'dueCards-title'}}
|
||||
li
|
||||
a.js-global-search(href="{{pathFor 'global-search'}}")
|
||||
i.fa.fa-search
|
||||
| {{_ 'globalSearch-title'}}
|
||||
li
|
||||
a.js-broken-cards(href="{{pathFor 'broken-cards'}}")
|
||||
i.fa.fa-chain-broken
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue