From 9a52bb2d2ca80bd3920f05594938928a6f6c9e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 20 Sep 2019 15:19:15 -0400 Subject: [PATCH 1/7] Permissive value in filter --- CHANGELOG.md | 3 + src/core/syntax-tree/index.ts | 2 +- src/core/syntax-tree/lexicon.ts | 1 + .../syntax-tree/lexeme/expression.ts | 63 ++++++++++++------- src/dash-table/syntax-tree/lexicon/column.ts | 6 +- .../unit/single_column_syntactic_tree_test.ts | 8 +++ 6 files changed, 55 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4724a721..f1f6333ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). [#546](https://github.com/plotly/dash-table/issues/546) - New prop `export_columns` that takes values `all` or `visible` (default). This prop controls the columns used during export +[#598](https://github.com/plotly/dash-table/issues/598) +- Allow values with whitespaces in column filters + ### Fixed [#460](https://github.com/plotly/dash-table/issues/460) - The `datestartswith` relational operator now supports number comparison diff --git a/src/core/syntax-tree/index.ts b/src/core/syntax-tree/index.ts index bf3e3568f..c689ddc8a 100644 --- a/src/core/syntax-tree/index.ts +++ b/src/core/syntax-tree/index.ts @@ -79,7 +79,7 @@ export default class SyntaxTree { toQueryString() { return this.lexerResult.valid ? - R.map(l => l.value, this.lexerResult.lexemes).join(' ') : + R.map(l => l.lexeme.transform ? l.lexeme.transform(l.value) : l.value, this.lexerResult.lexemes).join(' ') : ''; } diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index f1bb0907c..e0eb5b473 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -21,6 +21,7 @@ export interface IUnboundedLexeme { regexp: RegExp; regexpMatch?: number; syntaxer?: (lexs: any[], pivot: any, pivotIndex: number) => any; + transform?: (value: any) => any; } export interface ILexeme extends IUnboundedLexeme { diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index fd7032ae4..9cb9bbb4a 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -6,6 +6,7 @@ import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; const FIELD_REGEX = /^{(([^{}\\]|\\.)+)}/; const STRING_REGEX = /^(('([^'\\]|\\.)*')|("([^"\\]|\\.)*")|(`([^`\\]|\\.)*`))/; const VALUE_REGEX = /^(([^\s'"`{}()\\]|\\.)+)(?:[\s)]|$)/; +const PERMISSIVE_VALUE_REGEX = /^(([^'"`{}()\\]|\\.)+)(?:$)/; const getField = ( value: string @@ -31,16 +32,6 @@ const getString = ( value: string ) => value.slice(1, value.length - 1).replace(/\\(.)/g, '$1'); -const getValue = ( - value: string -) => { - value = (value.match(VALUE_REGEX) as any)[1]; - - return isNumeric(value) ? - +value : - value.replace(/\\(.)/g, '$1'); -}; - export const stringExpression: IUnboundedLexeme = { present: (tree: ISyntaxTree) => getString(tree.value), resolve: (_target: any, tree: ISyntaxTree) => { @@ -55,17 +46,41 @@ export const stringExpression: IUnboundedLexeme = { type: LexemeType.Expression }; -export const valueExpression: IUnboundedLexeme = { - present: (tree: ISyntaxTree) => getValue(tree.value), - resolve: (_target: any, tree: ISyntaxTree) => { - if (VALUE_REGEX.test(tree.value)) { - return getValue(tree.value); - } else { - throw new Error(); - } - }, - regexp: VALUE_REGEX, - regexpMatch: 1, - subType: 'value', - type: LexemeType.Expression -}; \ No newline at end of file +const getValueFactory = (regex: RegExp) => (value: string) => { + value = (value.match(regex) as any)[1]; + + return isNumeric(value) ? + +value : + value.replace(/\\(.)/g, '$1'); +}; + +const valueExpressionFactory = ( + regex: RegExp, + transform?: (v: any) => any +): IUnboundedLexeme => { + const getValue = getValueFactory(regex); + + return { + present: (tree: ISyntaxTree) => getValue(tree.value), + resolve: (_target: any, tree: ISyntaxTree) => { + if (regex.test(tree.value)) { + return getValue(tree.value); + } else { + throw new Error(); + } + }, + regexp: regex, + regexpMatch: 1, + subType: 'value', + transform, + type: LexemeType.Expression + }; +}; + +export const valueExpression = valueExpressionFactory(VALUE_REGEX); +export const permissiveValueExpression = valueExpressionFactory( + PERMISSIVE_VALUE_REGEX, + (v: any) => typeof v === 'string' && v.indexOf(' ') !== -1 ? + `"${v}"` : + v +); \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index b73c85231..936e04050 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -1,7 +1,7 @@ import { fieldExpression, stringExpression, - valueExpression + permissiveValueExpression } from '../lexeme/expression'; import { contains, @@ -60,8 +60,8 @@ const lexicon: ILexeme[] = [ })), ...[ fieldExpression, - stringExpression, - valueExpression + permissiveValueExpression, + stringExpression ].map(exp => ({ ...exp, if: ifExpression, diff --git a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts index 8582c1e10..2dbb59891 100644 --- a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts @@ -97,6 +97,14 @@ describe('Single Column Syntax Tree', () => { expect(tree.toQueryString()).to.equal('{a} = 1'); }); + it.only('can be permissive value expression', () => { + const tree = new SingleColumnSyntaxTree('Hello world', COLUMN_TEXT); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'Hello world' })).to.equal(true); + expect(tree.evaluate({ a: 'Helloworld' })).to.equal(false); + }); + it('`undefined` column type can use `contains`', () => { const tree = new SingleColumnSyntaxTree('contains 1', COLUMN_UNDEFINED); From 4f99218899e68e21de6e0a0a99d60c78c731bdc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 20 Sep 2019 15:34:24 -0400 Subject: [PATCH 2/7] add `is blank` unary operator --- CHANGELOG.md | 3 +++ src/dash-table/syntax-tree/lexeme/unary.ts | 7 ++++++- src/dash-table/syntax-tree/lexicon/column.ts | 4 +++- src/dash-table/syntax-tree/lexicon/columnMulti.ts | 4 +++- src/dash-table/syntax-tree/lexicon/query.ts | 4 +++- tests/cypress/tests/unit/query_syntactic_tree_test.ts | 10 ++++++++++ 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f6333ea..5ff99ebe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). [#598](https://github.com/plotly/dash-table/issues/598) - Allow values with whitespaces in column filters +[#597](https://github.com/plotly/dash-table/issues/597) +- Add `is blank` unary operator. Returns true for `undefined`, `null` and `''`. + ### Fixed [#460](https://github.com/plotly/dash-table/issues/460) - The `datestartswith` relational operator now supports number comparison diff --git a/src/dash-table/syntax-tree/lexeme/unary.ts b/src/dash-table/syntax-tree/lexeme/unary.ts index aab1d5f52..67a969ecf 100644 --- a/src/dash-table/syntax-tree/lexeme/unary.ts +++ b/src/dash-table/syntax-tree/lexeme/unary.ts @@ -30,7 +30,7 @@ function relationalSyntaxer([left, lexeme]: any[]) { } function relationalEvaluator( - fn: (opValue: any[]) => boolean + fn: (opValue: any) => boolean ) { return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree)); } @@ -75,6 +75,11 @@ export const isEven: IUnboundedLexeme = R.merge({ regexp: /^(is even)/i }, LEXEME_BASE); +export const isBlank: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(opValue => opValue === undefined || opValue === null || opValue === ''), + regexp: /^(is blank)/i +}, LEXEME_BASE); + export const isNil: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(opValue => opValue === undefined || opValue === null), regexp: /^(is nil)/i diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index 936e04050..e8bfaf08a 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -14,6 +14,7 @@ import { notEqual } from '../lexeme/relational'; import { + isBlank, isBool, isEven, isNil, @@ -45,7 +46,8 @@ const lexicon: ILexeme[] = [ if: ifLeading, terminal: false })), - ...[isBool, + ...[isBlank, + isBool, isEven, isNil, isNum, diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index 43406f7df..f3e138fee 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -19,6 +19,7 @@ import { notEqual } from '../lexeme/relational'; import { + isBlank, isBool, isEven, isNil, @@ -56,7 +57,8 @@ const lexicon: ILexeme[] = [ if: ifRelationalOperator, terminal: false })), - ...[isBool, + ...[isBlank, + isBool, isEven, isNil, isNum, diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index 4dfe66c9a..a53698a65 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -27,6 +27,7 @@ import { notEqual } from '../lexeme/relational'; import { + isBlank, isBool, isEven, isNil, @@ -89,7 +90,8 @@ const lexicon: ILexeme[] = [ if: ifRelationalOperator, terminal: false })), - ...[isBool, + ...[isBlank, + isBool, isEven, isNil, isNum, diff --git a/tests/cypress/tests/unit/query_syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts index 9af69d3e4..f99010eea 100644 --- a/tests/cypress/tests/unit/query_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -338,6 +338,16 @@ describe('Query Syntax Tree', () => { expect(tree.evaluate(data3)).to.equal(false); }); + it('can check blank', () => { + const tree = new QuerySyntaxTree('{d} is blank'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(true); + expect(tree.evaluate(data1)).to.equal(false); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); + it('can invert check nil', () => { const tree = new QuerySyntaxTree('!({d} is nil)'); From 43f177dca4ac36a9995b2fc63645f99228eeee89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 20 Sep 2019 15:36:46 -0400 Subject: [PATCH 3/7] changelog --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff99ebe3..a53184006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ This project adheres to [Semantic Versioning](http://semver.org/). [#546](https://github.com/plotly/dash-table/issues/546) - New prop `export_columns` that takes values `all` or `visible` (default). This prop controls the columns used during export -[#598](https://github.com/plotly/dash-table/issues/598) -- Allow values with whitespaces in column filters - [#597](https://github.com/plotly/dash-table/issues/597) - Add `is blank` unary operator. Returns true for `undefined`, `null` and `''`. +### Changed +[#598](https://github.com/plotly/dash-table/issues/598) +- Allow values with whitespaces in column filters + ### Fixed [#460](https://github.com/plotly/dash-table/issues/460) - The `datestartswith` relational operator now supports number comparison From 732d447a6e23b4d58dbc165b2bb808d202105897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 20 Sep 2019 16:13:01 -0400 Subject: [PATCH 4/7] update test for permissive value syntax --- tests/cypress/tests/standalone/filtering_test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts index ba9dae5fb..0f8a15782 100644 --- a/tests/cypress/tests/standalone/filtering_test.ts +++ b/tests/cypress/tests/standalone/filtering_test.ts @@ -59,7 +59,7 @@ describe('filter', () => { DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', 'lt 20000')); }); - it('handles invalid queries', () => { + it('allows permissive syntax queries but not invalid queries', () => { let cell_0; let cell_1; @@ -89,10 +89,10 @@ describe('filter', () => { DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', '20 a000')); DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', 'is prime2')); - DashTable.getFilterById('bbb').should('have.class', 'invalid'); + DashTable.getFilterById('bbb').should('have.class', 'valid'); DashTable.getFilterById('ccc').should('have.class', 'invalid'); - DashTable.getFilterById('ddd').should('have.class', 'invalid'); - DashTable.getFilterById('eee').should('have.class', 'invalid'); + DashTable.getFilterById('ddd').should('have.class', 'valid'); + DashTable.getFilterById('eee').should('have.class', 'valid'); }); it('filters `Text` columns with `contains` without operator', () => { From 3827e77ba8420d5d2ff3be54d32e76cd4b8f321c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 20 Sep 2019 17:17:43 -0400 Subject: [PATCH 5/7] update filtering test --- tests/cypress/tests/standalone/filtering_test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts index 0f8a15782..6a23a6e62 100644 --- a/tests/cypress/tests/standalone/filtering_test.ts +++ b/tests/cypress/tests/standalone/filtering_test.ts @@ -59,7 +59,7 @@ describe('filter', () => { DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', 'lt 20000')); }); - it('allows permissive syntax queries but not invalid queries', () => { + it('handles invalid queries', () => { let cell_0; let cell_1; @@ -74,25 +74,25 @@ describe('filter', () => { DashTable.getFilterById('ccc').click(); DOM.focused.type(`gt`); DashTable.getFilterById('ddd').click(); - DOM.focused.type('20 a000'); + DOM.focused.type('"20 a000'); DashTable.getFilterById('eee').click(); DOM.focused.type('is prime2'); DashTable.getFilterById('bbb').click(); - DOM.focused.type('! !'); + DOM.focused.type('! !"'); DashTable.getFilterById('ccc').click(); DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_0)); DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_1)); - DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', '! !')); + DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', '! !"')); DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt')); - DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', '20 a000')); + DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', '"20 a000')); DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', 'is prime2')); - DashTable.getFilterById('bbb').should('have.class', 'valid'); + DashTable.getFilterById('bbb').should('have.class', 'invalid'); DashTable.getFilterById('ccc').should('have.class', 'invalid'); - DashTable.getFilterById('ddd').should('have.class', 'valid'); - DashTable.getFilterById('eee').should('have.class', 'valid'); + DashTable.getFilterById('ddd').should('have.class', 'invalid'); + DashTable.getFilterById('eee').should('have.class', 'invalid'); }); it('filters `Text` columns with `contains` without operator', () => { From 7c5883b8ae862b5d680e3ccecc8bf20c8bf15064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Rivet?= Date: Tue, 24 Sep 2019 06:54:40 -0400 Subject: [PATCH 6/7] Update src/dash-table/syntax-tree/lexeme/expression.ts Co-Authored-By: alexcjohnson --- src/dash-table/syntax-tree/lexeme/expression.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index 9cb9bbb4a..3842f24bb 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -6,7 +6,7 @@ import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; const FIELD_REGEX = /^{(([^{}\\]|\\.)+)}/; const STRING_REGEX = /^(('([^'\\]|\\.)*')|("([^"\\]|\\.)*")|(`([^`\\]|\\.)*`))/; const VALUE_REGEX = /^(([^\s'"`{}()\\]|\\.)+)(?:[\s)]|$)/; -const PERMISSIVE_VALUE_REGEX = /^(([^'"`{}()\\]|\\.)+)(?:$)/; +const PERMISSIVE_VALUE_REGEX = /^(([^'"`{}()\\]|\\.)+)$/; const getField = ( value: string @@ -83,4 +83,4 @@ export const permissiveValueExpression = valueExpressionFactory( (v: any) => typeof v === 'string' && v.indexOf(' ') !== -1 ? `"${v}"` : v -); \ No newline at end of file +); From 824e7df0c7bf2a56ea1405213400081661eb2f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 24 Sep 2019 09:16:27 -0400 Subject: [PATCH 7/7] - add toQueryString test for permissive value --- tests/cypress/tests/unit/single_column_syntactic_tree_test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts index 2dbb59891..5218ced39 100644 --- a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts @@ -103,6 +103,7 @@ describe('Single Column Syntax Tree', () => { expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 'Hello world' })).to.equal(true); expect(tree.evaluate({ a: 'Helloworld' })).to.equal(false); + expect(tree.toQueryString()).to.equal('{a} contains "Hello world"'); }); it('`undefined` column type can use `contains`', () => {