Skip to content

Commit 728b5bb

Browse files
Improve filtering type awareness (plotly#410)
1 parent 2be5cec commit 728b5bb

29 files changed

+627
-289
lines changed

packages/dash-table/.circleci/config.yml

+71-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: 2
22

33
jobs:
4-
"test":
4+
"server-test":
55
docker:
66
- image: circleci/python:3.6.7-node-browsers
77
- image: cypress/base:10
@@ -44,7 +44,73 @@ jobs:
4444
name: Run tests
4545
command: |
4646
. venv/bin/activate
47-
npm run test
47+
npm run test.server
48+
49+
50+
"standalone-test":
51+
docker:
52+
- image: circleci/python:3.6.7-node-browsers
53+
- image: cypress/base:10
54+
55+
steps:
56+
- checkout
57+
- restore_cache:
58+
key: deps1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
59+
- run:
60+
name: Install npm packages
61+
command: npm install
62+
- run:
63+
name: Cypress Install
64+
command: |
65+
$(npm bin)/cypress install
66+
67+
- save_cache:
68+
key: deps1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
69+
paths:
70+
- node_modules
71+
- /home/circleci/.cache/Cypress
72+
73+
- run:
74+
name: Run tests
75+
command: npm run test.standalone
76+
77+
78+
"unit-test":
79+
docker:
80+
- image: circleci/python:3.6.7-node-browsers
81+
- image: cypress/base:10
82+
83+
steps:
84+
- checkout
85+
- restore_cache:
86+
key: deps1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
87+
- run:
88+
name: Install npm packages
89+
command: npm install
90+
- run:
91+
name: Cypress Install
92+
command: |
93+
$(npm bin)/cypress install
94+
95+
- save_cache:
96+
key: deps1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
97+
paths:
98+
- node_modules
99+
- /home/circleci/.cache/Cypress
100+
101+
- run:
102+
name: Install requirements
103+
command: |
104+
sudo pip install --upgrade virtualenv
105+
python -m venv venv || virtualenv venv
106+
. venv/bin/activate
107+
pip install -r requirements.txt --quiet
108+
109+
- run:
110+
name: Run tests
111+
command: |
112+
. venv/bin/activate
113+
npm run test.unit
48114
49115
50116
"visual-test":
@@ -170,5 +236,7 @@ workflows:
170236
jobs:
171237
- "python-3.6"
172238
- "node"
173-
- "test"
239+
- "server-test"
240+
- "standalone-test"
241+
- "unit-test"
174242
- "visual-test"

packages/dash-table/CHANGELOG.md

+8-4
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [Unreleased]
66
### Added
7-
[#397](https://github.com/plotly/dash-table/pull/397)
7+
[#397](https://github.com/plotly/dash-table/pull/397), [#410](https://github.com/plotly/dash-table/pull/410)
88
- Improve filtering syntax and capabilities
99
- new field syntax `{myField}`
1010
- short form by-column filter
11-
- implicit column and `eq` operator (e.g `"value"`)
12-
- implicit column (e.g `ne "value"`)
13-
- explicit form (e.g `{field} ne "value"`)
11+
- implicit column and default operator based on column type
12+
- Text and Any columns default to `contains`
13+
- Numeric columns default to `eq`
14+
- Date columns default to `datestartswith`
15+
- implicit column (e.g `ne "value"` becomes `{my-column} ne "value"`)
1416
- new `contains` relational operator for strings
17+
- new `datestartswith` relational operator for dates
18+
- new `eq` behavior (will attempt to convert and compare numeric values if possible)
1519
- new readonly `derived_filter_structure` prop exposing the query structure in a programmatically friendlier way
1620

1721
### Changed

packages/dash-table/package.json

+12-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"main": "dash_table/bundle.js",
66
"scripts": {
77
"preprivate::opentests": "run-s private::wait*",
8-
"preprivate::runtests": "run-s private::wait*",
8+
"preprivate::test.server": "run-s private::wait_dash*",
9+
"preprivate::test.standalone": "run-s private::wait_js",
10+
"pretest.standalone": "run-s private::build:js-test",
911
"private::build": "node --max_old_space_size=4096 node_modules/webpack/bin/webpack --display-reasons --bail",
1012
"private::build:js": "run-s \"private::build -- --mode production\"",
1113
"private::build:js-dev": "run-s \"private::build -- --mode development\"",
@@ -26,15 +28,16 @@
2628
"private::wait_dash8083": "wait-on http://localhost:8083",
2729
"private::wait_js": "wait-on http://localhost:8080",
2830
"private::opentests": "cypress open",
29-
"private::runtests:python": "python -m unittest tests/unit/format_test.py",
30-
"private::runtests:unit": "cypress run --browser chrome --spec 'tests/cypress/tests/unit/**/*'",
31-
"private::runtests:standalone": "cypress run --browser chrome --spec 'tests/cypress/tests/standalone/**/*'",
32-
"private::runtests:server": "cypress run --browser chrome --spec 'tests/cypress/tests/server/**/*'",
33-
"private::runtests": "run-s private::runtests:python private::runtests:unit private::runtests:standalone private::runtests:server",
31+
"private::test.python": "python -m unittest tests/unit/format_test.py",
32+
"private::test.unit": "cypress run --browser chrome --spec 'tests/cypress/tests/unit/**/*'",
33+
"private::test.server": "cypress run --browser chrome --spec 'tests/cypress/tests/server/**/*'",
34+
"private::test.standalone": "cypress run --browser chrome --spec 'tests/cypress/tests/standalone/**/*'",
3435
"build.watch": "webpack-dev-server --content-base dash_table --mode development",
3536
"build": "run-s private::build:js private::build:py",
3637
"lint": "run-s private::lint:*",
37-
"test": "run-p --race private::host* private::runtests",
38+
"test.server": "run-p --race private::host* private::test.server",
39+
"test.standalone": "run-p --race private::host_js private::test.standalone",
40+
"test.unit": "run-s private::test.python private::test.unit",
3841
"test.visual": "build-storybook && percy-storybook",
3942
"test.visual-local": "build-storybook",
4043
"test.watch": "run-p --race \"private::build:js-test-watch\" --race private::host* private::opentests"
@@ -54,7 +57,7 @@
5457
"@storybook/react": "^5.0.5",
5558
"@types/d3-format": "^1.3.1",
5659
"@types/papaparse": "^4.5.9",
57-
"@types/ramda": "^0.26.5",
60+
"@types/ramda": "^0.26.6",
5861
"@types/react": "^16.8.8",
5962
"@types/react-dom": "^16.8.3",
6063
"@types/react-select": "^1.3.4",
@@ -82,7 +85,7 @@
8285
"style-loader": "^0.23.1",
8386
"ts-loader": "^5.3.3",
8487
"tslint": "^5.14.0",
85-
"typescript": "^3.3.4000",
88+
"typescript": "^3.4.3",
8689
"wait-on": "^3.2.0",
8790
"webpack": "^4.29.6",
8891
"webpack-cli": "^3.3.0",

packages/dash-table/src/core/syntax-tree/lexer.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,20 @@ export default function lexer(lexicon: Lexicon, query: string): ILexerResult {
4242
query = query.substring(value.length);
4343
}
4444

45-
const last = result.slice(-1)[0];
46-
47-
const terminal: boolean = last && (typeof last.lexeme.terminal === 'function' ?
48-
last.lexeme.terminal(result, last) :
49-
last.lexeme.terminal);
45+
const [terminalPrevious, last] = [
46+
undefined,
47+
undefined,
48+
...result
49+
].slice(-2);
50+
51+
const terminal: boolean = !last ||
52+
(typeof last.lexeme.terminal === 'function' ?
53+
last.lexeme.terminal(result, terminalPrevious) :
54+
last.lexeme.terminal
55+
);
5056

5157
return {
5258
lexemes: result,
53-
valid: !last || terminal
59+
valid: terminal
5460
};
5561
}

packages/dash-table/src/core/syntax-tree/lexicon.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ export enum LexemeType {
77
LogicalOperator = 'logical-operator',
88
RelationalOperator = 'relational-operator',
99
UnaryOperator = 'unary-operator',
10-
Expression = 'expression',
11-
Operand = 'operand'
10+
Expression = 'expression'
1211
}
1312

1413
export interface IUnboundedLexeme {
@@ -25,8 +24,8 @@ export interface IUnboundedLexeme {
2524
}
2625

2726
export interface ILexeme extends IUnboundedLexeme {
28-
terminal: boolean | ((lexemes: ILexemeResult[], previous: ILexemeResult) => boolean);
29-
if: (string | undefined)[] | ((lexemes: ILexemeResult[], previous: ILexemeResult) => boolean);
27+
terminal: boolean | ((lexemes: ILexemeResult[], previous: ILexemeResult | undefined) => boolean);
28+
if: (string | undefined)[] | ((lexemes: ILexemeResult[], previous: ILexemeResult | undefined) => boolean);
3029
}
3130

3231
export function boundLexeme(lexeme: IUnboundedLexeme) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type RequiredPluck<T, R extends keyof T> = { [r in R]: T[r] };
2+
export type OptionalPluck<T, R extends keyof T> = { [r in R]?: T[r] };

packages/dash-table/src/dash-table/components/FilterFactory.tsx

+14-14
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ export default class FilterFactory {
4848

4949
}
5050

51-
private onChange = (columnId: ColumnId, setFilter: SetFilter, ev: any) => {
52-
Logger.debug('Filter -- onChange', columnId, ev.target.value && ev.target.value.trim());
51+
private onChange = (column: IVisibleColumn, setFilter: SetFilter, ev: any) => {
52+
Logger.debug('Filter -- onChange', column.id, ev.target.value && ev.target.value.trim());
5353

5454
const value = ev.target.value.trim();
55-
const safeColumnId = columnId.toString();
55+
const safeColumnId = column.id.toString();
5656

5757
if (value && value.length) {
58-
this.ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, value));
58+
this.ops.set(safeColumnId, new SingleColumnSyntaxTree(value, column));
5959
} else {
6060
this.ops.delete(safeColumnId);
6161
}
@@ -65,26 +65,26 @@ export default class FilterFactory {
6565

6666
const rawGlobalFilter = R.map(
6767
ast => ast.query || '',
68-
R.filter(ast => Boolean(ast), asts)
68+
R.filter<SingleColumnSyntaxTree>(ast => Boolean(ast), asts)
6969
).join(' && ');
7070

7171
setFilter(globalFilter, rawGlobalFilter);
7272
}
7373

74-
private getEventHandler = (fn: Function, columnId: ColumnId, setFilter: SetFilter): any => {
74+
private getEventHandler = (fn: Function, column: IVisibleColumn, setFilter: SetFilter): any => {
7575
const fnHandler = (this.handlers.get(fn) || this.handlers.set(fn, new Map()).get(fn));
76-
const columnIdHandler = (fnHandler.get(columnId) || fnHandler.set(columnId, new Map()).get(columnId));
76+
const columnIdHandler = (fnHandler.get(column.id) || fnHandler.set(column.id, new Map()).get(column.id));
7777

7878
return (
7979
columnIdHandler.get(setFilter) ||
80-
(columnIdHandler.set(setFilter, fn.bind(this, columnId, setFilter)).get(setFilter))
80+
(columnIdHandler.set(setFilter, fn.bind(this, column, setFilter)).get(setFilter))
8181
);
8282
}
8383

84-
private updateOps = memoizeOne((query: string) => {
84+
private updateOps = memoizeOne((query: string, columns: IVisibleColumn[]) => {
8585
const multiQuery = new MultiColumnsSyntaxTree(query);
8686

87-
const newOps = getSingleColumnMap(multiQuery);
87+
const newOps = getSingleColumnMap(multiQuery, columns);
8888
if (!newOps) {
8989
return;
9090
}
@@ -109,15 +109,15 @@ export default class FilterFactory {
109109
});
110110

111111
private filter = memoizerCache<[ColumnId, number]>()((
112-
column: ColumnId,
112+
column: IVisibleColumn,
113113
index: number,
114114
ast: SingleColumnSyntaxTree | undefined,
115115
setFilter: SetFilter
116116
) => {
117117
return (<ColumnFilter
118118
key={`column-${index}`}
119119
classes={`dash-filter column-${index}`}
120-
columnId={column}
120+
columnId={column.id}
121121
isValid={!ast || ast.isValid}
122122
setFilter={this.getEventHandler(this.onChange, column, setFilter)}
123123
value={ast && ast.query}
@@ -143,7 +143,7 @@ export default class FilterFactory {
143143
return [];
144144
}
145145

146-
this.updateOps(filter);
146+
this.updateOps(filter, columns);
147147

148148
if (filtering_type === FilteringType.Basic) {
149149
const filterStyles = this.relevantStyles(
@@ -160,7 +160,7 @@ export default class FilterFactory {
160160

161161
const filters = R.addIndex<IVisibleColumn, JSX.Element>(R.map)((column, index) => {
162162
return this.filter.get(column.id, index)(
163-
column.id,
163+
column,
164164
index,
165165
this.ops.get(column.id.toString()),
166166
setFilter

packages/dash-table/src/dash-table/dash/DataTable.js

-3
Original file line numberDiff line numberDiff line change
@@ -992,18 +992,15 @@ export const propTypes = {
992992
* - 'relational-operator'
993993
* - 'unary-operator'
994994
* - 'expression'
995-
* - 'operand'
996995
* - subType (string; optional)
997996
* - 'open-block': '()'
998997
* - 'logical-operator': '&&', '||'
999998
* - 'relational-operator': '=', '>=', '>', '<=', '<', '!=', 'contains'
1000999
* - 'unary-operator': '!', 'is bool', 'is even', 'is nil', 'is num', 'is object', 'is odd', 'is prime', 'is str'
10011000
* - 'expression': 'value', 'field'
1002-
* - 'operand': 'field'
10031001
* - value (any)
10041002
* - 'expression, value': passed value
10051003
* - 'expression, field': the field/prop name
1006-
* - 'operand, field': the field/prop name
10071004
*
10081005
* - block (nested query structure; optional)
10091006
* - left (nested query structure; optional)

packages/dash-table/src/dash-table/derived/cell/dropdowns.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class Dropdowns {
9696
...(staticDropdown ? [staticDropdown] : []),
9797
...R.map(
9898
([cd]) => cd.dropdown,
99-
R.filter(
99+
R.filter<[IConditionalDropdown, number]>(
100100
([cd, i]) => this.evaluation.get(column.id, i)(
101101
this.ast.get(column.id, i)(cd.condition),
102102
datum

packages/dash-table/src/dash-table/derived/cell/wrapperStyles.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function getter(
1616
return R.addIndex<any, Style[]>(R.map)((datum, index) => R.map(column => {
1717
const relevantStyles = R.map(
1818
s => s.style,
19-
R.filter(
19+
R.filter<IConvertedStyle>(
2020
style =>
2121
style.matchesColumn(column) &&
2222
style.matchesRow(index + offset.rows) &&

packages/dash-table/src/dash-table/derived/filter/wrapperStyles.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function getter(
1616
return R.map(column => {
1717
const relevantStyles = R.map(
1818
s => s.style,
19-
R.filter(
19+
R.filter<IConvertedStyle>(
2020
style => style.matchesColumn(column),
2121
filterStyles
2222
)

packages/dash-table/src/dash-table/derived/header/wrapperStyles.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function getter(
1717
return R.map(idx => R.map(column => {
1818
const relevantStyles = R.map(
1919
s => s.style,
20-
R.filter(
20+
R.filter<IConvertedStyle>(
2121
style =>
2222
style.matchesColumn(column) &&
2323
style.matchesRow(idx),

packages/dash-table/src/dash-table/syntax-tree/MultiColumnsSyntaxTree.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { LexemeType } from 'core/syntax-tree/lexicon';
55
import { ISyntaxTree } from 'core/syntax-tree/syntaxer';
66

77
import columnMultiLexicon from './lexicon/columnMulti';
8+
import { ILexemeResult } from 'core/syntax-tree/lexer';
89

910
export default class MultiColumnsSyntaxTree extends SyntaxTree {
1011
constructor(query: string) {
@@ -39,7 +40,13 @@ export default class MultiColumnsSyntaxTree extends SyntaxTree {
3940
return statements;
4041
}
4142
private respectsBasicSyntax() {
42-
const fields = R.map(item => item.value, R.filter(i => i.lexeme.type === LexemeType.Operand, this.lexerResult.lexemes));
43+
const fields = R.map(
44+
(item: ILexemeResult) => item.value,
45+
R.filter(
46+
i => i.lexeme.type === LexemeType.Expression && i.lexeme.subType === 'field',
47+
this.lexerResult.lexemes
48+
)
49+
);
4350
const uniqueFields = R.uniq(fields);
4451
return fields.length === uniqueFields.length;
4552
}

0 commit comments

Comments
 (0)