Browse Source

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

Lauri Ojansivu 7 years ago
parent
commit
e8206f2983

+ 10 - 0
client/components/cards/minicard.jade

@@ -20,10 +20,20 @@ template(name="minicard")
         .date
         .date
           +cardSpentTime
           +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
     if members
       .minicard-members.js-minicard-members
       .minicard-members.js-minicard-members
         each members
         each members
           +userAvatar(userId=this)
           +userAvatar(userId=this)
+
     .badges
     .badges
       if comments.count
       if comments.count
         .badge(title="{{_ 'card-comments-title' comments.count }}")
         .badge(title="{{_ 'card-comments-title' comments.count }}")

+ 7 - 0
client/components/cards/minicard.styl

@@ -77,6 +77,13 @@
       height: @width
       height: @width
       border-radius: 2px
       border-radius: 2px
       margin-left: 3px
       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
   .minicard-title
     p:last-child
     p:last-child
       margin-bottom: 0
       margin-bottom: 0

+ 4 - 0
client/components/sidebar/sidebarFilters.jade

@@ -55,6 +55,10 @@ template(name="filterSidebar")
             {{ name }}
             {{ name }}
           if Filter.customFields.isSelected _id
           if Filter.customFields.isSelected _id
               i.fa.fa-check
               i.fa.fa-check
+  hr
+  span {{_ 'advanced-filter-label'}}
+  input.js-field-advanced-filter(type="text")
+  span {{_ 'advanced-filter-description'}}
   if Filter.isActive
   if Filter.isActive
     hr
     hr
     a.sidebar-btn.js-clear-all
     a.sidebar-btn.js-clear-all

+ 5 - 0
client/components/sidebar/sidebarFilters.js

@@ -16,6 +16,11 @@ BlazeComponent.extendComponent({
         Filter.customFields.toggle(this.currentData()._id);
         Filter.customFields.toggle(this.currentData()._id);
         Filter.resetExceptions();
         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) {
       'click .js-clear-all'(evt) {
         evt.preventDefault();
         evt.preventDefault();
         Filter.reset();
         Filter.reset();

+ 308 - 5
client/lib/filter.js

@@ -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.
 // The global Filter object.
 // XXX It would be possible to re-write this object more elegantly, and removing
 // 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
 // the need to provide a list of `_fields`. We also should move methods into the
@@ -90,6 +386,7 @@ Filter = {
   labelIds: new SetFilter(),
   labelIds: new SetFilter(),
   members: new SetFilter(),
   members: new SetFilter(),
   customFields: new SetFilter('_id'),
   customFields: new SetFilter('_id'),
+  advanced: new AdvancedFilter(),
 
 
   _fields: ['labelIds', 'members', 'customFields'],
   _fields: ['labelIds', 'members', 'customFields'],
 
 
@@ -102,7 +399,7 @@ Filter = {
   isActive() {
   isActive() {
     return _.any(this._fields, (fieldName) => {
     return _.any(this._fields, (fieldName) => {
       return this[fieldName]._isActive();
       return this[fieldName]._isActive();
-    });
+    }) || this.advanced._isActive();
   },
   },
 
 
   _getMongoSelector() {
   _getMongoSelector() {
@@ -133,10 +430,15 @@ Filter = {
     const exceptionsSelector = {_id: {$in: this._exceptions}};
     const exceptionsSelector = {_id: {$in: this._exceptions}};
     this._exceptionsDep.depend();
     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) {
   mongoSelector(additionalSelector) {
@@ -152,6 +454,7 @@ Filter = {
       const filter = this[fieldName];
       const filter = this[fieldName];
       filter.reset();
       filter.reset();
     });
     });
+    this.advanced.reset();
     this.resetExceptions();
     this.resetExceptions();
   },
   },
 
 

+ 2 - 0
i18n/en.i18n.json

@@ -246,6 +246,8 @@
     "filter-on": "Filter is on",
     "filter-on": "Filter is on",
     "filter-on-desc": "You are filtering cards on this board. Click here to edit filter.",
     "filter-on-desc": "You are filtering cards on this board. Click here to edit filter.",
     "filter-to-selection": "Filter to selection",
     "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",
     "fullname": "Full Name",
     "header-logo-title": "Go back to your boards page.",
     "header-logo-title": "Go back to your boards page.",
     "hide-system-messages": "Hide system messages",
     "hide-system-messages": "Hide system messages",