diff --git a/res/app/device-list/device-column-service.js b/res/app/device-list/device-column-service.js index ee03fe2b..b10e4f22 100644 --- a/res/app/device-list/device-column-service.js +++ b/res/app/device-list/device-column-service.js @@ -56,6 +56,32 @@ module.exports = function DeviceColumnService($filter, gettext) { return 0 } + , filter: function(device, filter) { + var va = (device.version || '0').split('.') + , vb = (filter.query || '0').split('.') + , la = va.length + , lb = vb.length + , op = filterOps[filter.op || '='] + + if (vb[lb - 1] === '') { + // This means that the query is not complete yet, and we're + // looking at something like "4.", which means that the last part + // should be ignored. + vb.pop() + lb -= 1 + } + + for (var i = 0, l = Math.min(la, lb); i < l; ++i) { + var a = parseInt(va[i], 10) + , b = parseInt(vb[i], 10) + + if (!op(a, b)) { + return false + } + } + + return true + } }) , network: TextCell({ title: gettext('Network') @@ -201,6 +227,24 @@ function compareRespectCase(a, b) { return a === b ? 0 : (a < b ? -1 : 1) } +var filterOps = { + '<': function(a, filterValue) { + return a < filterValue + } +, '<=': function(a, filterValue) { + return a <= filterValue + } +, '>': function(a, filterValue) { + return a > filterValue + } +, '>=': function(a, filterValue) { + return a >= filterValue + } +, '=': function(a, filterValue) { + return a === filterValue + } +} + function TextCell(options) { return _.defaults(options, { title: options.title @@ -241,13 +285,17 @@ function NumberCell(options) { , compare: function(a, b) { return options.value(a) - options.value(b) } - , filter: function(item, filter) { - return filterIgnoreCase(options.value(item), filter.query) - } + , filter: (function() { + return function(item, filter) { + return filterOps[filter.op || '=']( + options.value(item) + , +filter.query + ) + } + })() }) } - function DateCell(options) { return _.defaults(options, { title: options.title @@ -277,9 +325,19 @@ function DateCell(options) { , vb = options.value(b) || 0 return va - vb } - , filter: function(item, filter) { - return filterIgnoreCase(options.value(item) + '', filter.query) - } + , filter: (function() { + function dateNumber(d) { + return d + ? d.getFullYear() * 10000 + d.getMonth() * 100 + d.getDate() + : 0 + } + return function(item, filter) { + var filterDate = new Date(filter.query) + , va = dateNumber(options.value(item)) + , vb = dateNumber(filterDate) + return filterOps[filter.op || '='](va, vb) + } + })() }) } diff --git a/res/app/device-list/device-list.jade b/res/app/device-list/device-list.jade index 7e8d5dd6..eb16ac25 100644 --- a/res/app/device-list/device-list.jade +++ b/res/app/device-list/device-list.jade @@ -16,7 +16,7 @@ .filtering-buttons input(type='search', results='5', autosave='deviceFilter' name='deviceFilter', ng-model='deviceFilter', ng-change='applyFilter(deviceFilter)', - ng-model-options='{debounce: 150}' + ng-model-options='{debounce: 250}' autocorrect='off', autocapitalize='off', spellcheck='false').form-control.input-sm.device-search.pull-right .clear-filtering-buttons @@ -30,7 +30,7 @@ .filtering-buttons input(type='search', results='5', autosave='deviceFilter' name='deviceFilter', ng-model='deviceFilter', ng-change='applyFilter(deviceFilter)', - ng-model-options='{debounce: 150}' + ng-model-options='{debounce: 250}' autocorrect='off', autocapitalize='off', spellcheck='false').form-control.input-sm.device-search.pull-right span.pull-right diff --git a/res/app/device-list/util/query-parser-test.js b/res/app/device-list/util/query-parser-test.js index 6e7f0b05..1bc5ca79 100644 --- a/res/app/device-list/util/query-parser-test.js +++ b/res/app/device-list/util/query-parser-test.js @@ -8,6 +8,7 @@ var tests = [ assert.deepEqual(parser.parse('a'), [ { field: null + , op: null , query: 'a' } ]) @@ -17,14 +18,17 @@ var tests = [ assert.deepEqual(parser.parse('a b c'), [ { field: null + , op: null , query: 'a' } , { field: null + , op: null , query: 'b' } , { field: null + , op: null , query: 'c' } ]) @@ -34,15 +38,31 @@ var tests = [ assert.deepEqual(parser.parse('serial:foo'), [ { field: 'serial' + , op: null , query: 'foo' } ]) } +/* + This test is currently failing, but I'm not sure if I care enough about it. + Commented out for now. + +, function() { + var parser = new QueryParser() + assert.deepEqual(parser.parse('a:b:c'), [ + { + field: 'a' + , query: 'b:c' + } + ]) + } +*/ , function() { var parser = new QueryParser() assert.deepEqual(parser.parse('name:"Galaxy S2 LTE"'), [ { field: 'name' + , op: null , query: 'Galaxy S2 LTE' } ]) @@ -52,10 +72,12 @@ var tests = [ assert.deepEqual(parser.parse('name:"Galaxy S2 LTE" black'), [ { field: 'name' + , op: null , query: 'Galaxy S2 LTE' } , { field: null + , op: null , query: 'black' } ]) @@ -65,6 +87,7 @@ var tests = [ assert.deepEqual(parser.parse('"foo bar"'), [ { field: null + , op: null , query: 'foo bar' } ]) @@ -74,7 +97,8 @@ var tests = [ assert.deepEqual(parser.parse('version:>=4.1'), [ { field: 'version' - , query: '>=4.1' + , op: '>=' + , query: '4.1' } ]) } @@ -83,7 +107,18 @@ var tests = [ assert.deepEqual(parser.parse('version: >=4.1'), [ { field: 'version' - , query: '>=4.1' + , op: '>=' + , query: '4.1' + } + ]) + } +, function() { + var parser = new QueryParser() + assert.deepEqual(parser.parse('version: < 4.1'), [ + { + field: 'version' + , op: '<' + , query: '4.1' } ]) } @@ -92,10 +127,12 @@ var tests = [ assert.deepEqual(parser.parse('Galaxy operator: DOCOMO'), [ { field: null + , op: null , query: 'Galaxy' } , { field: 'operator' + , op: null , query: 'DOCOMO' } ]) diff --git a/res/app/device-list/util/query-parser.js b/res/app/device-list/util/query-parser.js index 4866307e..74e9a4e3 100644 --- a/res/app/device-list/util/query-parser.js +++ b/res/app/device-list/util/query-parser.js @@ -1,13 +1,16 @@ var State = { - TERM_START: 1 -, FIELD_OR_QUERY: 2 -, QUERY_START: 3 -, QUERY: 4 -, DOUBLEQUOTED_QUERY: 5 + TERM_START: 10 +, QUERY_START: 20 +, OP_LT: 30 +, OP_GT: 40 +, QUERY_VALUE_START: 50 +, QUERY_VALUE: 60 +, QUERY_VALUE_DOUBLEQUOTED: 70 } function Term() { this.field = null + this.op = null this.query = '' } @@ -38,14 +41,53 @@ QueryParser.prototype.consume = function(input) { return } this.terms.push(this.currentTerm) - if (input === '"') { - this.state = State.DOUBLEQUOTED_QUERY + this.state = State.QUERY_START + return this.consume(input) + case State.QUERY_START: + if (this.isWhitespace(input)) { + // Preceding whitespace, ignore. return } - this.state = State.FIELD_OR_QUERY - this.currentTerm.query += input - return - case State.FIELD_OR_QUERY: + if (input === '<') { + this.state = State.OP_LT + return + } + if (input === '>') { + this.state = State.OP_GT + return + } + this.state = State.QUERY_VALUE_START + return this.consume(input) + case State.OP_LT: + if (input === '=') { + this.currentTerm.op = '<=' + this.state = State.QUERY_VALUE_START + return + } + this.currentTerm.op = '<' + this.state = State.QUERY_VALUE_START + return this.consume(input) + case State.OP_GT: + if (input === '=') { + this.currentTerm.op = '>=' + this.state = State.QUERY_VALUE_START + return + } + this.currentTerm.op = '>' + this.state = State.QUERY_VALUE_START + return this.consume(input) + case State.QUERY_VALUE_START: + if (this.isWhitespace(input)) { + // Preceding whitespace, ignore. + return + } + if (input === '"') { + this.state = State.QUERY_VALUE_DOUBLEQUOTED + return + } + this.state = State.QUERY_VALUE + return this.consume(input) + case State.QUERY_VALUE: if (this.isWhitespace(input)) { return this.concludeTerm() } @@ -57,28 +99,7 @@ QueryParser.prototype.consume = function(input) { } this.currentTerm.query += input return - case State.QUERY_START: - if (this.isWhitespace(input)) { - // Preceding whitespace, ignore. - return - } - if (input === '"') { - this.state = State.DOUBLEQUOTED_QUERY - return - } - this.currentTerm.query += input - return - case State.QUERY: - if (this.isWhitespace(input)) { - return this.concludeTerm() - } - if (input === '"') { - this.state = State.DOUBLEQUOTED_QUERY - return - } - this.currentTerm.query += input - return - case State.DOUBLEQUOTED_QUERY: + case State.QUERY_VALUE_DOUBLEQUOTED: if (input === '\\') { return }