Skip to content
This repository was archived by the owner on Jun 4, 2024. It is now read-only.

Commit 9beb39c

Browse files
Issue 598 - Permissive value in filter (#599)
1 parent 224a27c commit 9beb39c

File tree

11 files changed

+89
-36
lines changed

11 files changed

+89
-36
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).
77
[#546](https://github.com/plotly/dash-table/issues/546)
88
- New prop `export_columns` that takes values `all` or `visible` (default). This prop controls the columns used during export
99

10+
[#597](https://github.com/plotly/dash-table/issues/597)
11+
- Add `is blank` unary operator. Returns true for `undefined`, `null` and `''`.
12+
13+
### Changed
14+
[#598](https://github.com/plotly/dash-table/issues/598)
15+
- Allow values with whitespaces in column filters
16+
1017
### Fixed
1118
[#460](https://github.com/plotly/dash-table/issues/460)
1219
- The `datestartswith` relational operator now supports number comparison

src/core/syntax-tree/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export default class SyntaxTree {
7979

8080
toQueryString() {
8181
return this.lexerResult.valid ?
82-
R.map(l => l.value, this.lexerResult.lexemes).join(' ') :
82+
R.map(l => l.lexeme.transform ? l.lexeme.transform(l.value) : l.value, this.lexerResult.lexemes).join(' ') :
8383
'';
8484
}
8585

src/core/syntax-tree/lexicon.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface IUnboundedLexeme {
2121
regexp: RegExp;
2222
regexpMatch?: number;
2323
syntaxer?: (lexs: any[], pivot: any, pivotIndex: number) => any;
24+
transform?: (value: any) => any;
2425
}
2526

2627
export interface ILexeme extends IUnboundedLexeme {

src/dash-table/syntax-tree/lexeme/expression.ts

+39-24
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ISyntaxTree } from 'core/syntax-tree/syntaxer';
66
const FIELD_REGEX = /^{(([^{}\\]|\\.)+)}/;
77
const STRING_REGEX = /^(('([^'\\]|\\.)*')|("([^"\\]|\\.)*")|(`([^`\\]|\\.)*`))/;
88
const VALUE_REGEX = /^(([^\s'"`{}()\\]|\\.)+)(?:[\s)]|$)/;
9+
const PERMISSIVE_VALUE_REGEX = /^(([^'"`{}()\\]|\\.)+)$/;
910

1011
const getField = (
1112
value: string
@@ -31,16 +32,6 @@ const getString = (
3132
value: string
3233
) => value.slice(1, value.length - 1).replace(/\\(.)/g, '$1');
3334

34-
const getValue = (
35-
value: string
36-
) => {
37-
value = (value.match(VALUE_REGEX) as any)[1];
38-
39-
return isNumeric(value) ?
40-
+value :
41-
value.replace(/\\(.)/g, '$1');
42-
};
43-
4435
export const stringExpression: IUnboundedLexeme = {
4536
present: (tree: ISyntaxTree) => getString(tree.value),
4637
resolve: (_target: any, tree: ISyntaxTree) => {
@@ -55,17 +46,41 @@ export const stringExpression: IUnboundedLexeme = {
5546
type: LexemeType.Expression
5647
};
5748

58-
export const valueExpression: IUnboundedLexeme = {
59-
present: (tree: ISyntaxTree) => getValue(tree.value),
60-
resolve: (_target: any, tree: ISyntaxTree) => {
61-
if (VALUE_REGEX.test(tree.value)) {
62-
return getValue(tree.value);
63-
} else {
64-
throw new Error();
65-
}
66-
},
67-
regexp: VALUE_REGEX,
68-
regexpMatch: 1,
69-
subType: 'value',
70-
type: LexemeType.Expression
71-
};
49+
const getValueFactory = (regex: RegExp) => (value: string) => {
50+
value = (value.match(regex) as any)[1];
51+
52+
return isNumeric(value) ?
53+
+value :
54+
value.replace(/\\(.)/g, '$1');
55+
};
56+
57+
const valueExpressionFactory = (
58+
regex: RegExp,
59+
transform?: (v: any) => any
60+
): IUnboundedLexeme => {
61+
const getValue = getValueFactory(regex);
62+
63+
return {
64+
present: (tree: ISyntaxTree) => getValue(tree.value),
65+
resolve: (_target: any, tree: ISyntaxTree) => {
66+
if (regex.test(tree.value)) {
67+
return getValue(tree.value);
68+
} else {
69+
throw new Error();
70+
}
71+
},
72+
regexp: regex,
73+
regexpMatch: 1,
74+
subType: 'value',
75+
transform,
76+
type: LexemeType.Expression
77+
};
78+
};
79+
80+
export const valueExpression = valueExpressionFactory(VALUE_REGEX);
81+
export const permissiveValueExpression = valueExpressionFactory(
82+
PERMISSIVE_VALUE_REGEX,
83+
(v: any) => typeof v === 'string' && v.indexOf(' ') !== -1 ?
84+
`"${v}"` :
85+
v
86+
);

src/dash-table/syntax-tree/lexeme/unary.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function relationalSyntaxer([left, lexeme]: any[]) {
3030
}
3131

3232
function relationalEvaluator(
33-
fn: (opValue: any[]) => boolean
33+
fn: (opValue: any) => boolean
3434
) {
3535
return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree));
3636
}
@@ -75,6 +75,11 @@ export const isEven: IUnboundedLexeme = R.merge({
7575
regexp: /^(is even)/i
7676
}, LEXEME_BASE);
7777

78+
export const isBlank: IUnboundedLexeme = R.merge({
79+
evaluate: relationalEvaluator(opValue => opValue === undefined || opValue === null || opValue === ''),
80+
regexp: /^(is blank)/i
81+
}, LEXEME_BASE);
82+
7883
export const isNil: IUnboundedLexeme = R.merge({
7984
evaluate: relationalEvaluator(opValue => opValue === undefined || opValue === null),
8085
regexp: /^(is nil)/i

src/dash-table/syntax-tree/lexicon/column.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
fieldExpression,
33
stringExpression,
4-
valueExpression
4+
permissiveValueExpression
55
} from '../lexeme/expression';
66
import {
77
contains,
@@ -14,6 +14,7 @@ import {
1414
notEqual
1515
} from '../lexeme/relational';
1616
import {
17+
isBlank,
1718
isBool,
1819
isEven,
1920
isNil,
@@ -45,7 +46,8 @@ const lexicon: ILexeme[] = [
4546
if: ifLeading,
4647
terminal: false
4748
})),
48-
...[isBool,
49+
...[isBlank,
50+
isBool,
4951
isEven,
5052
isNil,
5153
isNum,
@@ -60,8 +62,8 @@ const lexicon: ILexeme[] = [
6062
})),
6163
...[
6264
fieldExpression,
63-
stringExpression,
64-
valueExpression
65+
permissiveValueExpression,
66+
stringExpression
6567
].map(exp => ({
6668
...exp,
6769
if: ifExpression,

src/dash-table/syntax-tree/lexicon/columnMulti.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
notEqual
2020
} from '../lexeme/relational';
2121
import {
22+
isBlank,
2223
isBool,
2324
isEven,
2425
isNil,
@@ -56,7 +57,8 @@ const lexicon: ILexeme[] = [
5657
if: ifRelationalOperator,
5758
terminal: false
5859
})),
59-
...[isBool,
60+
...[isBlank,
61+
isBool,
6062
isEven,
6163
isNil,
6264
isNum,

src/dash-table/syntax-tree/lexicon/query.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
notEqual
2828
} from '../lexeme/relational';
2929
import {
30+
isBlank,
3031
isBool,
3132
isEven,
3233
isNil,
@@ -89,7 +90,8 @@ const lexicon: ILexeme[] = [
8990
if: ifRelationalOperator,
9091
terminal: false
9192
})),
92-
...[isBool,
93+
...[isBlank,
94+
isBool,
9395
isEven,
9496
isNil,
9597
isNum,

tests/cypress/tests/standalone/filtering_test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,19 @@ describe('filter', () => {
7474
DashTable.getFilterById('ccc').click();
7575
DOM.focused.type(`gt`);
7676
DashTable.getFilterById('ddd').click();
77-
DOM.focused.type('20 a000');
77+
DOM.focused.type('"20 a000');
7878
DashTable.getFilterById('eee').click();
7979
DOM.focused.type('is prime2');
8080
DashTable.getFilterById('bbb').click();
81-
DOM.focused.type('! !');
81+
DOM.focused.type('! !"');
8282
DashTable.getFilterById('ccc').click();
8383

8484
DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_0));
8585
DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_1));
8686

87-
DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', '! !'));
87+
DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', '! !"'));
8888
DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt'));
89-
DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', '20 a000'));
89+
DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', '"20 a000'));
9090
DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', 'is prime2'));
9191

9292
DashTable.getFilterById('bbb').should('have.class', 'invalid');

tests/cypress/tests/unit/query_syntactic_tree_test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,16 @@ describe('Query Syntax Tree', () => {
338338
expect(tree.evaluate(data3)).to.equal(false);
339339
});
340340

341+
it('can check blank', () => {
342+
const tree = new QuerySyntaxTree('{d} is blank');
343+
344+
expect(tree.isValid).to.equal(true);
345+
expect(tree.evaluate(data0)).to.equal(true);
346+
expect(tree.evaluate(data1)).to.equal(false);
347+
expect(tree.evaluate(data2)).to.equal(true);
348+
expect(tree.evaluate(data3)).to.equal(false);
349+
});
350+
341351
it('can invert check nil', () => {
342352
const tree = new QuerySyntaxTree('!({d} is nil)');
343353

tests/cypress/tests/unit/single_column_syntactic_tree_test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ describe('Single Column Syntax Tree', () => {
9797
expect(tree.toQueryString()).to.equal('{a} = 1');
9898
});
9999

100+
it.only('can be permissive value expression', () => {
101+
const tree = new SingleColumnSyntaxTree('Hello world', COLUMN_TEXT);
102+
103+
expect(tree.isValid).to.equal(true);
104+
expect(tree.evaluate({ a: 'Hello world' })).to.equal(true);
105+
expect(tree.evaluate({ a: 'Helloworld' })).to.equal(false);
106+
expect(tree.toQueryString()).to.equal('{a} contains "Hello world"');
107+
});
108+
100109
it('`undefined` column type can use `contains`', () => {
101110
const tree = new SingleColumnSyntaxTree('contains 1', COLUMN_UNDEFINED);
102111

0 commit comments

Comments
 (0)