diff options
-rw-r--r-- | client/components/cards/minicard.jade | 10 | ||||
-rw-r--r-- | client/components/cards/minicard.styl | 7 | ||||
-rw-r--r-- | client/components/sidebar/sidebarFilters.jade | 4 | ||||
-rw-r--r-- | client/components/sidebar/sidebarFilters.js | 5 | ||||
-rw-r--r-- | client/lib/filter.js | 313 | ||||
-rw-r--r-- | i18n/en.i18n.json | 2 |
6 files changed, 336 insertions, 5 deletions
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index 9fa4dd57..aa0708dd 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -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 }}") diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl index d59f1f63..38f829d0 100644 --- a/client/components/cards/minicard.styl +++ b/client/components/cards/minicard.styl @@ -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 diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade index 5f9fcf72..514870b8 100644 --- a/client/components/sidebar/sidebarFilters.jade +++ b/client/components/sidebar/sidebarFilters.jade @@ -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 diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index ba2633de..fd8229e4 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/client/components/sidebar/sidebarFilters.js @@ -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(); diff --git a/client/lib/filter.js b/client/lib/filter.js index f68c9711..c5f8fe7e 100644 --- a/client/lib/filter.js +++ b/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. // 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(); }, diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index e2d6ddce..f44dba23 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -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", |