Merge branch 'feature-advanced-filter' of https://github.com/feuerball11/wekan into feuerball11-feature-advanced-filter

This commit is contained in:
Lauri Ojansivu 2018-05-20 22:53:36 +03:00
commit e8206f2983
6 changed files with 336 additions and 5 deletions

View file

@ -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 }}")

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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();
},

View file

@ -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",