mirror of
https://github.com/wekan/wekan.git
synced 2025-04-22 04:57:07 -04:00
Merge branch 'feature-advanced-filter' of https://github.com/feuerball11/wekan into feuerball11-feature-advanced-filter
This commit is contained in:
commit
e8206f2983
6 changed files with 336 additions and 5 deletions
|
@ -20,10 +20,20 @@ template(name="minicard")
|
|||
.date
|
||||
+cardSpentTime
|
||||
|
||||
.minicard-custom-fields
|
||||
each customFieldsWD
|
||||
if definition.showOnCard
|
||||
.minicard-custom-field
|
||||
.minicard-custom-field-item
|
||||
= definition.name
|
||||
.minicard-custom-field-item
|
||||
= value
|
||||
|
||||
if members
|
||||
.minicard-members.js-minicard-members
|
||||
each members
|
||||
+userAvatar(userId=this)
|
||||
|
||||
.badges
|
||||
if comments.count
|
||||
.badge(title="{{_ 'card-comments-title' comments.count }}")
|
||||
|
|
|
@ -77,6 +77,13 @@
|
|||
height: @width
|
||||
border-radius: 2px
|
||||
margin-left: 3px
|
||||
.minicard-custom-fields
|
||||
display:block;
|
||||
.minicard-custom-field
|
||||
display:flex;
|
||||
.minicard-custom-field-item
|
||||
max-width:50%;
|
||||
flex-grow:1;
|
||||
.minicard-title
|
||||
p:last-child
|
||||
margin-bottom: 0
|
||||
|
|
|
@ -55,6 +55,10 @@ template(name="filterSidebar")
|
|||
{{ name }}
|
||||
if Filter.customFields.isSelected _id
|
||||
i.fa.fa-check
|
||||
hr
|
||||
span {{_ 'advanced-filter-label'}}
|
||||
input.js-field-advanced-filter(type="text")
|
||||
span {{_ 'advanced-filter-description'}}
|
||||
if Filter.isActive
|
||||
hr
|
||||
a.sidebar-btn.js-clear-all
|
||||
|
|
|
@ -16,6 +16,11 @@ BlazeComponent.extendComponent({
|
|||
Filter.customFields.toggle(this.currentData()._id);
|
||||
Filter.resetExceptions();
|
||||
},
|
||||
'change .js-field-advanced-filter'(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.advanced.set(this.find('.js-field-advanced-filter').value.trim());
|
||||
Filter.resetExceptions();
|
||||
},
|
||||
'click .js-clear-all'(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.reset();
|
||||
|
|
|
@ -79,6 +79,302 @@ class SetFilter {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Advanced filter forms a MongoSelector from a users String.
|
||||
// Build by: Ignatz 19.05.2018 (github feuerball11)
|
||||
class AdvancedFilter {
|
||||
constructor() {
|
||||
this._dep = new Tracker.Dependency();
|
||||
this._filter = '';
|
||||
this._lastValide={};
|
||||
}
|
||||
|
||||
set(str)
|
||||
{
|
||||
this._filter = str;
|
||||
this._dep.changed();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._filter = '';
|
||||
this._lastValide={};
|
||||
this._dep.changed();
|
||||
}
|
||||
|
||||
_isActive() {
|
||||
this._dep.depend();
|
||||
return this._filter !== '';
|
||||
}
|
||||
|
||||
_filterToCommands(){
|
||||
const commands = [];
|
||||
let current = '';
|
||||
let string = false;
|
||||
let wasString = false;
|
||||
let ignore = false;
|
||||
for (let i = 0; i < this._filter.length; i++)
|
||||
{
|
||||
const char = this._filter.charAt(i);
|
||||
if (ignore)
|
||||
{
|
||||
ignore = false;
|
||||
continue;
|
||||
}
|
||||
if (char === '\'')
|
||||
{
|
||||
string = !string;
|
||||
if (string) wasString = true;
|
||||
continue;
|
||||
}
|
||||
if (char === '\\')
|
||||
{
|
||||
ignore = true;
|
||||
continue;
|
||||
}
|
||||
if (char === ' ' && !string)
|
||||
{
|
||||
commands.push({'cmd':current, 'string':wasString});
|
||||
wasString = false;
|
||||
current = '';
|
||||
continue;
|
||||
}
|
||||
current += char;
|
||||
}
|
||||
if (current !== '')
|
||||
{
|
||||
commands.push({'cmd':current, 'string':wasString});
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
_fieldNameToId(field)
|
||||
{
|
||||
const found = CustomFields.findOne({'name':field});
|
||||
return found._id;
|
||||
}
|
||||
|
||||
_arrayToSelector(commands)
|
||||
{
|
||||
try {
|
||||
//let changed = false;
|
||||
this._processSubCommands(commands);
|
||||
}
|
||||
catch (e){return this._lastValide;}
|
||||
this._lastValide = {$or: commands};
|
||||
return {$or: commands};
|
||||
}
|
||||
|
||||
_processSubCommands(commands)
|
||||
{
|
||||
const subcommands = [];
|
||||
let level = 0;
|
||||
let start = -1;
|
||||
for (let i = 0; i < commands.length; i++)
|
||||
{
|
||||
if (commands[i].cmd)
|
||||
{
|
||||
switch (commands[i].cmd)
|
||||
{
|
||||
case '(':
|
||||
{
|
||||
level++;
|
||||
if (start === -1) start = i;
|
||||
continue;
|
||||
}
|
||||
case ')':
|
||||
{
|
||||
level--;
|
||||
commands.splice(i, 1);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (level > 0)
|
||||
{
|
||||
subcommands.push(commands[i]);
|
||||
commands.splice(i, 1);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (start !== -1)
|
||||
{
|
||||
this._processSubCommands(subcommands);
|
||||
if (subcommands.length === 1)
|
||||
commands.splice(start, 0, subcommands[0]);
|
||||
else
|
||||
commands.splice(start, 0, subcommands);
|
||||
}
|
||||
this._processConditions(commands);
|
||||
this._processLogicalOperators(commands);
|
||||
}
|
||||
|
||||
_processConditions(commands)
|
||||
{
|
||||
for (let i = 0; i < commands.length; i++)
|
||||
{
|
||||
if (!commands[i].string && commands[i].cmd)
|
||||
{
|
||||
switch (commands[i].cmd)
|
||||
{
|
||||
case '=':
|
||||
case '==':
|
||||
case '===':
|
||||
{
|
||||
const field = commands[i-1].cmd;
|
||||
const str = commands[i+1].cmd;
|
||||
commands[i] = {'customFields._id':this._fieldNameToId(field), 'customFields.value':str};
|
||||
commands.splice(i-1, 1);
|
||||
commands.splice(i, 1);
|
||||
//changed = true;
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
case '!=':
|
||||
case '!==':
|
||||
{
|
||||
const field = commands[i-1].cmd;
|
||||
const str = commands[i+1].cmd;
|
||||
commands[i] = {'customFields._id':this._fieldNameToId(field), 'customFields.value': { $not: str }};
|
||||
commands.splice(i-1, 1);
|
||||
commands.splice(i, 1);
|
||||
//changed = true;
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
case '>':
|
||||
case 'gt':
|
||||
case 'Gt':
|
||||
case 'GT':
|
||||
{
|
||||
const field = commands[i-1].cmd;
|
||||
const str = commands[i+1].cmd;
|
||||
commands[i] = {'customFields._id':this._fieldNameToId(field), 'customFields.value': { $gt: str } };
|
||||
commands.splice(i-1, 1);
|
||||
commands.splice(i, 1);
|
||||
//changed = true;
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
case '>=':
|
||||
case '>==':
|
||||
case 'gte':
|
||||
case 'Gte':
|
||||
case 'GTE':
|
||||
{
|
||||
const field = commands[i-1].cmd;
|
||||
const str = commands[i+1].cmd;
|
||||
commands[i] = {'customFields._id':this._fieldNameToId(field), 'customFields.value': { $gte: str } };
|
||||
commands.splice(i-1, 1);
|
||||
commands.splice(i, 1);
|
||||
//changed = true;
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
case '<':
|
||||
case 'lt':
|
||||
case 'Lt':
|
||||
case 'LT':
|
||||
{
|
||||
const field = commands[i-1].cmd;
|
||||
const str = commands[i+1].cmd;
|
||||
commands[i] = {'customFields._id':this._fieldNameToId(field), 'customFields.value': { $lt: str } };
|
||||
commands.splice(i-1, 1);
|
||||
commands.splice(i, 1);
|
||||
//changed = true;
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
case '<=':
|
||||
case '<==':
|
||||
case 'lte':
|
||||
case 'Lte':
|
||||
case 'LTE':
|
||||
{
|
||||
const field = commands[i-1].cmd;
|
||||
const str = commands[i+1].cmd;
|
||||
commands[i] = {'customFields._id':this._fieldNameToId(field), 'customFields.value': { $lte: str } };
|
||||
commands.splice(i-1, 1);
|
||||
commands.splice(i, 1);
|
||||
//changed = true;
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_processLogicalOperators(commands)
|
||||
{
|
||||
for (let i = 0; i < commands.length; i++)
|
||||
{
|
||||
if (!commands[i].string && commands[i].cmd)
|
||||
{
|
||||
switch (commands[i].cmd)
|
||||
{
|
||||
case 'or':
|
||||
case 'Or':
|
||||
case 'OR':
|
||||
case '|':
|
||||
case '||':
|
||||
{
|
||||
const op1 = commands[i-1];
|
||||
const op2 = commands[i+1];
|
||||
commands[i] = {$or: [op1, op2]};
|
||||
commands.splice(i-1, 1);
|
||||
commands.splice(i, 1);
|
||||
//changed = true;
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
case 'and':
|
||||
case 'And':
|
||||
case 'AND':
|
||||
case '&':
|
||||
case '&&':
|
||||
{
|
||||
const op1 = commands[i-1];
|
||||
const op2 = commands[i+1];
|
||||
commands[i] = {$and: [op1, op2]};
|
||||
commands.splice(i-1, 1);
|
||||
commands.splice(i, 1);
|
||||
//changed = true;
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'not':
|
||||
case 'Not':
|
||||
case 'NOT':
|
||||
case '!':
|
||||
{
|
||||
const op1 = commands[i+1];
|
||||
commands[i] = {$not: op1};
|
||||
commands.splice(i+1, 1);
|
||||
//changed = true;
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getMongoSelector() {
|
||||
this._dep.depend();
|
||||
const commands = this._filterToCommands();
|
||||
return this._arrayToSelector(commands);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// The global Filter object.
|
||||
// XXX It would be possible to re-write this object more elegantly, and removing
|
||||
// the need to provide a list of `_fields`. We also should move methods into the
|
||||
|
@ -90,6 +386,7 @@ Filter = {
|
|||
labelIds: new SetFilter(),
|
||||
members: new SetFilter(),
|
||||
customFields: new SetFilter('_id'),
|
||||
advanced: new AdvancedFilter(),
|
||||
|
||||
_fields: ['labelIds', 'members', 'customFields'],
|
||||
|
||||
|
@ -102,7 +399,7 @@ Filter = {
|
|||
isActive() {
|
||||
return _.any(this._fields, (fieldName) => {
|
||||
return this[fieldName]._isActive();
|
||||
});
|
||||
}) || this.advanced._isActive();
|
||||
},
|
||||
|
||||
_getMongoSelector() {
|
||||
|
@ -133,10 +430,15 @@ Filter = {
|
|||
const exceptionsSelector = {_id: {$in: this._exceptions}};
|
||||
this._exceptionsDep.depend();
|
||||
|
||||
if (includeEmptySelectors)
|
||||
return {$or: [filterSelector, exceptionsSelector, emptySelector]};
|
||||
else
|
||||
return {$or: [filterSelector, exceptionsSelector]};
|
||||
const selectors = [exceptionsSelector];
|
||||
|
||||
if (_.any(this._fields, (fieldName) => {
|
||||
return this[fieldName]._isActive();
|
||||
})) selectors.push(filterSelector);
|
||||
if (includeEmptySelectors) selectors.push(emptySelector);
|
||||
if (this.advanced._isActive()) selectors.push(this.advanced._getMongoSelector());
|
||||
|
||||
return {$or: selectors};
|
||||
},
|
||||
|
||||
mongoSelector(additionalSelector) {
|
||||
|
@ -152,6 +454,7 @@ Filter = {
|
|||
const filter = this[fieldName];
|
||||
filter.reset();
|
||||
});
|
||||
this.advanced.reset();
|
||||
this.resetExceptions();
|
||||
},
|
||||
|
||||
|
|
|
@ -246,6 +246,8 @@
|
|||
"filter-on": "Filter is on",
|
||||
"filter-on-desc": "You are filtering cards on this board. Click here to edit filter.",
|
||||
"filter-to-selection": "Filter to selection",
|
||||
"advanced-filter-label": "Advanced Filter",
|
||||
"advanced-filter-description": "Advanced Filter allows to write a string containing following operators: == != <= >= && || ( ) A Space is used as seperator between the operators. You can filter for all custom fields by simply typing there names and values. For example: Field1 == Value1 Note: If fields or values contains spaces, you need to encapsulate them into single quetes. For example: 'Field 1' == 'Value 1' Also you can combine multiple Conditions. For Example: F1 == V1 || F1 = V2 Normaly all Operators are interpreted from left to right. You can change the order of that by placing brakets. For Example: F1 == V1 and ( F2 == V2 || F2 == V3 )",
|
||||
"fullname": "Full Name",
|
||||
"header-logo-title": "Go back to your boards page.",
|
||||
"hide-system-messages": "Hide system messages",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue