diff --git a/.circleci/config.yml b/.circleci/config.yml
index f10b22159..619292432 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,7 +1,7 @@
version: 2
jobs:
- "test-v0":
+ "test":
docker:
- image: circleci/python:3.6.7-node-browsers
- image: cypress/base:10
@@ -30,67 +30,7 @@ jobs:
sudo pip install --upgrade virtualenv
python -m venv venv || virtualenv venv
. venv/bin/activate
- pip install -r requirements-base.txt --quiet
-
- - run:
- name: Install dependencies (dash)
- command: |
- git clone git@github.com:plotly/dash.git
- git clone git@github.com:plotly/dash-renderer.git
- git clone git@github.com:plotly/dash-core-components.git
- git clone git@github.com:plotly/dash-html-components.git
- . venv/bin/activate
- pip install -e ./dash --quiet
- cd dash-renderer && npm install --ignore-scripts && npm run build && pip install -e . && cd ..
- cd dash-core-components && npm install --ignore-scripts && npm run build && pip install -e . && cd ..
- cd dash-html-components && npm install --ignore-scripts && npm run build && pip install -e . && cd ..
-
- - run:
- name: Build
- command: |
- . venv/bin/activate
- npm run private::build:js-test
- npm run private::build:py
- pip install -e .
-
- - run:
- name: Run tests
- command: |
- . venv/bin/activate
- npm run test-v0
-
-
- "test-v1":
- docker:
- - image: circleci/python:3.6.7-node-browsers
- - image: cypress/base:10
-
- steps:
- - checkout
- - restore_cache:
- key: deps1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
- - run:
- name: Install npm packages
- command: npm install
- - run:
- name: Cypress Install
- command: |
- $(npm bin)/cypress install
-
- - save_cache:
- key: deps1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
- paths:
- - node_modules
- - /home/circleci/.cache/Cypress
-
- - run:
- name: Install requirements
- command: |
- sudo pip install --upgrade virtualenv
- python -m venv venv || virtualenv venv
- . venv/bin/activate
- pip install -r requirements-base.txt --quiet
- pip install -r requirements-v1.txt --quiet
+ pip install -r requirements.txt --quiet
- run:
name: Build
@@ -104,7 +44,7 @@ jobs:
name: Run tests
command: |
. venv/bin/activate
- npm run test-v1
+ npm run test
"visual-test":
@@ -172,7 +112,7 @@ jobs:
when: always
- "python-3.6-v0":
+ "python-3.6":
docker:
- image: circleci/python:3.6.7-stretch-node-browsers
@@ -224,53 +164,11 @@ jobs:
python -m unittest tests.dash.test_integration
- "python-3.6-v1":
- docker:
- - image: circleci/python:3.6.7-stretch-node-browsers
-
- environment:
- PERCY_ENABLED: True
- PERCY_PROJECT: plotly/dash-table-python-v1
-
- steps:
- - checkout
-
- - run:
- name: Inject Percy Environment variables
- command: |
- echo 'export PERCY_TOKEN="$PERCY_PYTHON_TOKEN_V1"' >> $BASH_ENV
-
- - run:
- name: Install requirements
- command: |
- sudo pip install --upgrade virtualenv
- python -m venv venv || virtualenv venv
- . venv/bin/activate
- pip install -r requirements-base.txt --quiet
- pip install -r requirements-v1.txt --quiet
- npm install
-
- - run:
- name: Install test requirements
- command: |
- . venv/bin/activate
- npm run build
- pip install -e .
-
- - run:
- name: Run integration tests
- command: |
- . venv/bin/activate
- python -m unittest tests.dash.test_integration
-
-
workflows:
version: 2
build:
jobs:
- - "python-3.6-v0"
- - "python-3.6-v1"
+ - "python-3.6"
- "node"
- - "test-v0"
- - "test-v1"
+ - "test"
- "visual-test"
diff --git a/.config/webpack/base.js b/.config/webpack/base.js
index 32c6d61f0..362c631f9 100644
--- a/.config/webpack/base.js
+++ b/.config/webpack/base.js
@@ -43,6 +43,10 @@ module.exports = (preprocessor = {}, mode = 'development') => {
test: /demo[/\\]index.html?$/,
loader: 'file-loader?name=index.[ext]'
},
+ {
+ test: /\.csv$/,
+ loader: 'raw-loader'
+ },
{
test: /\.ts(x?)$/,
exclude: /node_modules/,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01a4885e8..4cf9c7646 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
+## [Unreleased]
+### Added
+[#397](https://github.com/plotly/dash-table/pull/397)
+- Improve filtering syntax and capabilities
+ - new field syntax `{myField}`
+ - short form by-column filter
+ - implicit column and `eq` operator (e.g `"value"`)
+ - implicit column (e.g `ne "value"`)
+ - explicit form (e.g `{field} ne "value"`)
+ - new `contains` relational operator for strings
+ - new readonly `derived_filter_structure` prop exposing the query structure in a programmatically friendlier way
+
+### Changed
+[#397](https://github.com/plotly/dash-table/pull/397)
+- Rename `filtering_settings` to `filter`
+
## [3.6.0] - 2019-03-04
### Fixed
[#189](https://github.com/plotly/dash-table/issues/189)
diff --git a/demo/App.js b/demo/App.js
index ce51c534d..07dc6acc2 100644
--- a/demo/App.js
+++ b/demo/App.js
@@ -14,6 +14,7 @@ class App extends Component {
super();
this.state = AppState;
+ this.state.temp_filtering = '';
const setProps = memoizeOne(() => {
return newProps => {
@@ -33,15 +34,30 @@ class App extends Component {
const mode = Environment.searchParams.get('mode');
if (mode === AppMode.Filtering) {
- return ();
+ return (
+
+
+ this.setState({ temp_filtering: e.target.value })
+ }
+ onBlur={e => {
+ const tableProps = R.clone(this.state.tableProps);
+ tableProps.filter = e.target.value;
+
+ this.setState({ tableProps });
+ }} />
+
);
}
}
diff --git a/demo/AppMode.ts b/demo/AppMode.ts
index 631c922a4..277419d9a 100644
--- a/demo/AppMode.ts
+++ b/demo/AppMode.ts
@@ -65,7 +65,7 @@ function getBaseTableProps(mock: IDataMock) {
max_width: '1000px',
width: '1000px'
},
- style_data_conditional: [
+ style_cell_conditional: [
{ max_width: 150, min_width: 150, width: 150 },
{ if: { column_id: 'rows' }, max_width: 60, min_width: 60, width: 60 },
{ if: { column_id: 'bbb' }, max_width: 200, min_width: 200, width: 200 },
@@ -150,7 +150,7 @@ function getTooltipsState() {
state.tableProps.column_conditional_tooltips = [{
if: {
column_id: 'aaa-readonly',
- filter: `aaa is prime`
+ filter: `{aaa} is prime`
},
type: TooltipSyntax.Markdown,
value: `### Go Proverbs\nCapture three to get an eye`
diff --git a/package.json b/package.json
index 5f3905aeb..93c1f639b 100644
--- a/package.json
+++ b/package.json
@@ -30,13 +30,11 @@
"private::runtests:unit": "cypress run --browser chrome --spec 'tests/cypress/tests/unit/**/*'",
"private::runtests:standalone": "cypress run --browser chrome --spec 'tests/cypress/tests/standalone/**/*'",
"private::runtests:server": "cypress run --browser chrome --spec 'tests/cypress/tests/server/**/*'",
- "private::runtests-v0": "run-s private::runtests:server",
- "private::runtests-v1": "run-s private::runtests:python private::runtests:unit private::runtests:standalone private::runtests:server",
+ "private::runtests": "run-s private::runtests:python private::runtests:unit private::runtests:standalone private::runtests:server",
"build.watch": "webpack-dev-server --content-base dash_table --mode development",
"build": "run-s private::build:js private::build:py",
"lint": "run-s private::lint:*",
- "test-v0": "run-p --race private::host* private::runtests-v0",
- "test-v1": "run-p --race private::host* private::runtests-v1",
+ "test": "run-p --race private::host* private::runtests",
"test.visual": "build-storybook && percy-storybook",
"test.visual-local": "build-storybook",
"test.watch": "run-p --race \"private::build:js-test-watch\" --race private::host* private::opentests"
@@ -55,6 +53,7 @@
"@storybook/cli": "^5.0.5",
"@storybook/react": "^5.0.5",
"@types/d3-format": "^1.3.1",
+ "@types/papaparse": "^4.5.9",
"@types/ramda": "^0.26.5",
"@types/react": "^16.8.8",
"@types/react-dom": "^16.8.3",
@@ -71,7 +70,9 @@
"less-loader": "^4.1.0",
"npm": "^6.9.0",
"npm-run-all": "^4.1.5",
+ "papaparse": "^4.6.3",
"ramda": "^0.26.1",
+ "raw-loader": "^2.0.0",
"react": "16.8.5",
"react-docgen": "^4.1.0",
"react-dom": "16.8.5",
diff --git a/requirements-v1.txt b/requirements-v1.txt
deleted file mode 100644
index 5d3d4a3a7..000000000
--- a/requirements-v1.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-git+git://github.com/plotly/dash@release-v1#egg=dash
-git+git://github.com/plotly/dash-core-components@release-v1#egg=dash_core_components
-git+git://github.com/plotly/dash-html-components@release-v1#egg=dash_html_components
-git+git://github.com/plotly/dash-renderer@release-v1#egg=dash_renderer
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 690080fe8..5d17344a8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
-r requirements-base.txt
--r requirements-v1.txt
\ No newline at end of file
+-r requirements-v0.txt
\ No newline at end of file
diff --git a/src/core/syntax-tree/index.ts b/src/core/syntax-tree/index.ts
index 292d351d0..bf3e3568f 100644
--- a/src/core/syntax-tree/index.ts
+++ b/src/core/syntax-tree/index.ts
@@ -1,20 +1,63 @@
+import * as R from 'ramda';
+
import Logger from 'core/Logger';
-import lexer from 'core/syntax-tree/lexer';
-import syntaxer, { ISyntaxerResult } from 'core/syntax-tree/syntaxer';
+import lexer, { ILexerResult } from 'core/syntax-tree/lexer';
+import syntaxer, { ISyntaxerResult, ISyntaxTree } from 'core/syntax-tree/syntaxer';
+import { Lexicon } from './lexicon';
+
+interface IStructure {
+ subType?: string;
+ type: string;
+ value: any;
+
+ block?: IStructure;
+ left?: IStructure;
+ right?: IStructure;
+}
+
+function toStructure(tree: ISyntaxTree): IStructure {
+ const { block, left, lexeme, right, value } = tree;
+
+ const res: IStructure = {
+ subType: lexeme.subType,
+ type: lexeme.type,
+ value: lexeme.present ? lexeme.present(tree) : value
+ };
+
+ if (block) {
+ res.block = toStructure(block);
+ }
+
+ if (left) {
+ res.left = toStructure(left);
+ }
+
+ if (right) {
+ res.right = toStructure(right);
+ }
+
+ return res;
+}
export default class SyntaxTree {
- private result: ISyntaxerResult;
+ protected lexerResult: ILexerResult;
+ protected syntaxerResult: ISyntaxerResult;
get isValid() {
- return this.result.valid;
+ return this.syntaxerResult.valid;
}
private get tree() {
- return this.result.tree;
+ return this.syntaxerResult.tree;
}
- constructor(private readonly query: string) {
- this.result = syntaxer(lexer(this.query));
+ constructor(
+ public readonly lexicon: Lexicon,
+ public readonly query: string,
+ postProcessor: (res: ILexerResult) => ILexerResult = res => res
+ ) {
+ this.lexerResult = postProcessor(lexer(this.lexicon, this.query));
+ this.syntaxerResult = syntaxer(this.lexerResult);
}
evaluate = (target: any) => {
@@ -33,4 +76,18 @@ export default class SyntaxTree {
filter = (targets: any[]) => {
return targets.filter(this.evaluate);
}
+
+ toQueryString() {
+ return this.lexerResult.valid ?
+ R.map(l => l.value, this.lexerResult.lexemes).join(' ') :
+ '';
+ }
+
+ toStructure() {
+ if (!this.isValid || !this.syntaxerResult.tree) {
+ return null;
+ }
+
+ return toStructure(this.syntaxerResult.tree);
+ }
}
\ No newline at end of file
diff --git a/src/core/syntax-tree/lexer.ts b/src/core/syntax-tree/lexer.ts
index 6daeca311..c0fa303f5 100644
--- a/src/core/syntax-tree/lexer.ts
+++ b/src/core/syntax-tree/lexer.ts
@@ -1,4 +1,6 @@
-import Lexicon, { ILexeme } from 'core/syntax-tree/lexicon';
+import * as R from 'ramda';
+
+import { ILexeme, Lexicon } from 'core/syntax-tree/lexicon';
export interface ILexerResult {
lexemes: ILexemeResult[];
@@ -11,32 +13,43 @@ export interface ILexemeResult {
value?: string;
}
-export default function lexer(query: string): ILexerResult {
- let lexeme: ILexeme | null = null;
-
+export default function lexer(lexicon: Lexicon, query: string): ILexerResult {
let result: ILexemeResult[] = [];
+
while (query.length) {
query = query.replace(/^\s+/, '');
- let lexemes: ILexeme[] = Lexicon.filter(_lexeme =>
- lexeme &&
- _lexeme.when &&
- _lexeme.when.indexOf(lexeme.name) !== -1);
+ const previous = result.slice(-1)[0];
+ const previousLexeme = previous ? previous.lexeme : null;
- if (!lexemes.length) {
- lexemes = Lexicon;
- }
+ let lexemes: ILexeme[] = lexicon.filter(lexeme =>
+ lexeme.if &&
+ (!Array.isArray(lexeme.if) ?
+ lexeme.if(result, previous) :
+ (previousLexeme ?
+ lexeme.if && lexeme.if.indexOf(previousLexeme.type) !== -1 :
+ lexeme.if && lexeme.if.indexOf(undefined) !== -1))
+ );
- lexeme = lexemes.find(_lexeme => _lexeme.regexp.test(query)) || null;
- if (!lexeme) {
+ const next = R.find(lexeme => lexeme.regexp.test(query), lexemes);
+ if (!next) {
return { lexemes: result, valid: false, error: query };
}
- const value = (query.match(lexeme.regexp) || [])[0];
- result.push({ lexeme, value });
+ const value = (query.match(next.regexp) || [])[next.regexpMatch || 0];
+ result.push({ lexeme: next, value });
query = query.substring(value.length);
}
- return { lexemes: result, valid: true };
+ const last = result.slice(-1)[0];
+
+ const terminal: boolean = last && (typeof last.lexeme.terminal === 'function' ?
+ last.lexeme.terminal(result, last) :
+ last.lexeme.terminal);
+
+ return {
+ lexemes: result,
+ valid: !last || terminal
+ };
}
\ No newline at end of file
diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts
index 3be0a5912..6a26e1b77 100644
--- a/src/core/syntax-tree/lexicon.ts
+++ b/src/core/syntax-tree/lexicon.ts
@@ -1,246 +1,36 @@
-import Logger from 'core/Logger';
-import { ISyntaxTree } from 'core/syntax-tree/syntaxer';
+import { ILexemeResult } from './lexer';
+import { ISyntaxTree } from './syntaxer';
export enum LexemeType {
- And = 'and',
BlockClose = 'close-block',
BlockOpen = 'open-block',
- BinaryOperator = 'logical-binary-operator',
+ LogicalOperator = 'logical-operator',
+ RelationalOperator = 'relational-operator',
+ UnaryOperator = 'unary-operator',
Expression = 'expression',
- Or = 'or',
- Operand = 'operand',
- UnaryNot = 'unary-not',
- UnaryOperator = 'logical-unary-operator'
+ Operand = 'operand'
}
-export interface ILexeme {
+export interface IUnboundedLexeme {
evaluate?: (target: any, tree: ISyntaxTree) => boolean;
+ present?: (tree: ISyntaxTree) => any;
resolve?: (target: any, tree: ISyntaxTree) => any;
- name: string;
+ subType?: string;
+ type: string;
nesting?: number;
priority?: number;
regexp: RegExp;
+ regexpMatch?: number;
syntaxer?: (lexs: any[], pivot: any, pivotIndex: number) => any;
- when?: string[];
}
-const isPrime = (c: number) => {
- if (c === 2) { return true; }
- if (c < 2 || c % 2 === 0) { return false; }
- for (let n = 3; n * n <= c; n += 2) { if (c % n === 0) { return false; } }
- return true;
-};
-
-const operand = {
- resolve: (target: any, tree: ISyntaxTree) => {
- if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.")+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) {
- return target[
- tree.value.slice(1, tree.value.length - 1)
- ];
- } else if (/^(\w|[:.\-+])+$/.test(tree.value)) {
- return target[tree.value];
- }
- },
- regexp: /^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/
-};
-
-const expression = {
- resolve: (target: any, tree: ISyntaxTree) => {
- if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) {
- return tree.value.slice(1, tree.value.length - 1);
- } else if (/^(num|str)\(.*\)$/.test(tree.value)) {
- const res = tree.value.match(/^(\w+)\((.*)\)$/);
- if (res) {
- const [, op, value] = res;
-
- switch (op) {
- case 'num':
- return parseFloat(value);
- case 'str':
- default:
- return value;
- }
- } else {
- throw Error();
- }
- } else {
- return target[tree.value];
- }
- },
- regexp: /^(((num|str)\([^()]*\))|('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/
-};
-
-const lexicon: ILexeme[] = [
- {
- evaluate: (target, tree) => {
- Logger.trace('evalute -> &&', target, tree);
-
- const t = tree as any;
- const lv = t.left.lexeme.evaluate(target, t.left);
- const rv = t.right.lexeme.evaluate(target, t.right);
- return lv && rv;
- },
- name: LexemeType.And,
- priority: 2,
- regexp: /^(and\s|&&)/i,
- syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => {
- return Object.assign({
- left: lexs.slice(0, pivotIndex),
- right: lexs.slice(pivotIndex + 1)
- }, pivot);
- }
- },
- {
- evaluate: (target, tree) => {
- Logger.trace('evalute -> ||', target, tree);
-
- const t = tree as any;
-
- return t.left.lexeme.evaluate(target, t.left) ||
- t.right.lexeme.evaluate(target, t.right);
- },
- name: LexemeType.Or,
- priority: 3,
- regexp: /^(or\s|\|\|)/i,
- syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => {
- return Object.assign({
- left: lexs.slice(0, pivotIndex),
- right: lexs.slice(pivotIndex + 1)
- }, pivot);
- }
- },
- {
- name: LexemeType.BlockClose,
- nesting: -1,
- regexp: /^\)/
- },
- {
- evaluate: (target, tree) => {
- Logger.trace('evaluate -> ()', target, tree);
-
- const t = tree as any;
-
- return t.block.lexeme.evaluate(target, t.block);
- },
- name: LexemeType.BlockOpen,
- nesting: 1,
- priority: 1,
- regexp: /^\(/,
- syntaxer: (lexs: any[]) => {
- return Object.assign({
- block: lexs.slice(1, lexs.length - 1)
- }, lexs[0]);
- },
- when: [LexemeType.UnaryNot]
- },
- {
- ...operand,
- name: LexemeType.Operand
- },
- {
- evaluate: (target, tree) => {
- Logger.trace('evaluate -> binary', target, tree);
-
- const t = tree as any;
-
- const opValue = t.left.lexeme.resolve(target, t.left);
- const expValue = t.right.lexeme.resolve(target, t.right);
- Logger.trace(`opValue: ${opValue}, expValue: ${expValue}`);
-
- switch (tree.value.toLowerCase()) {
- case 'eq':
- case '=':
- return opValue === expValue;
- case 'gt':
- case '>':
- return opValue > expValue;
- case 'ge':
- case '>=':
- return opValue >= expValue;
- case 'lt':
- case '<':
- return opValue < expValue;
- case 'le':
- case '<=':
- return opValue <= expValue;
- case 'ne':
- case '!=':
- return opValue !== expValue;
- default:
- throw new Error();
- }
- },
- name: LexemeType.BinaryOperator,
- priority: 0,
- regexp: /^(>=|<=|>|<|!=|=|ge|le|gt|lt|eq|ne)/i,
- syntaxer: (lexs: any[]) => {
- let [left, lexeme, right] = lexs;
-
- return Object.assign({ left, right }, lexeme);
- },
- when: [LexemeType.Operand]
- },
- {
- evaluate: (target, tree) => {
- Logger.trace('evaluate -> unary', target, tree);
-
- const t = tree as any;
- const opValue = t.block.lexeme.resolve(target, t.block);
-
- switch (tree.value.toLowerCase()) {
- case 'is even':
- return typeof opValue === 'number' && opValue % 2 === 0;
- case 'is nil':
- return opValue === undefined || opValue === null;
- case 'is bool':
- return typeof opValue === 'boolean';
- case 'is odd':
- return typeof opValue === 'number' && opValue % 2 === 1;
- case 'is num':
- return typeof opValue === 'number';
- case 'is object':
- return opValue !== null && typeof opValue === 'object';
- case 'is str':
- return typeof opValue === 'string';
- case 'is prime':
- return typeof opValue === 'number' && isPrime(opValue);
- default:
- throw new Error();
- }
- },
- name: LexemeType.UnaryOperator,
- priority: 0,
- regexp: /^((is nil)|(is odd)|(is even)|(is bool)|(is num)|(is object)|(is str)|(is prime))/i,
- syntaxer: (lexs: any[]) => {
- let [block, lexeme] = lexs;
-
- return Object.assign({ block }, lexeme);
- },
- when: [LexemeType.Operand]
- },
- {
- evaluate: (target, tree) => {
- Logger.trace('evaluate -> unary not', target, tree);
-
- const t = tree as any;
+export interface ILexeme extends IUnboundedLexeme {
+ terminal: boolean | ((lexemes: ILexemeResult[], previous: ILexemeResult) => boolean);
+ if: (string | undefined)[] | ((lexemes: ILexemeResult[], previous: ILexemeResult) => boolean);
+}
- return !t.block.lexeme.evaluate(target, t.block);
- },
- name: LexemeType.UnaryNot,
- priority: 1.5,
- regexp: /^!/,
- syntaxer: (lexs: any[]) => {
- return Object.assign({
- block: lexs.slice(1, lexs.length)
- }, lexs[0]);
- },
- when: [LexemeType.UnaryNot]
- },
- {
- ...expression,
- name: LexemeType.Expression,
- when: [LexemeType.BinaryOperator]
- }
-];
+export function boundLexeme(lexeme: IUnboundedLexeme) {
+ return { ...lexeme, if: () => false, terminal: false };
+}
-export default lexicon;
\ No newline at end of file
+export type Lexicon = ILexeme[];
\ No newline at end of file
diff --git a/src/core/syntax-tree/syntaxer.ts b/src/core/syntax-tree/syntaxer.ts
index 8729ed1df..d420747e0 100644
--- a/src/core/syntax-tree/syntaxer.ts
+++ b/src/core/syntax-tree/syntaxer.ts
@@ -53,7 +53,7 @@ const parser = (lexs: ILexemeResult[]): ISyntaxTree => {
return tree;
} else {
- throw new Error(pivot.lexeme.name);
+ throw new Error(pivot.lexeme.type);
}
};
diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx
index 763eda7c3..a9d8dc57e 100644
--- a/src/dash-table/components/ControlledTable/index.tsx
+++ b/src/dash-table/components/ControlledTable/index.tsx
@@ -29,6 +29,8 @@ import { derivedTableStyle } from 'dash-table/derived/style';
import { IStyle } from 'dash-table/derived/style/props';
import TableTooltip from './fragments/TableTooltip';
+import queryLexicon from 'dash-table/syntax-tree/lexicon/query';
+
const sortNumerical = R.sort((a, b) => a - b);
const DEFAULT_STYLE = {
@@ -50,12 +52,12 @@ export default class ControlledTable extends PureComponent
this.updateStylesheet();
}
- getLexerResult = memoizeOne(lexer);
+ getLexerResult = memoizeOne(lexer.bind(undefined, queryLexicon));
get lexerResult() {
- const { filtering_settings } = this.props;
+ const { filter } = this.props;
- return this.getLexerResult(filtering_settings);
+ return this.getLexerResult(filter);
}
private updateStylesheet() {
@@ -533,7 +535,7 @@ export default class ControlledTable extends PureComponent
columns,
data,
editable,
- filtering_settings,
+ filter,
setProps,
sorting_settings,
viewport
@@ -550,7 +552,7 @@ export default class ControlledTable extends PureComponent
columns,
data,
true,
- !sorting_settings.length || !filtering_settings.length
+ !sorting_settings.length || !filter.length
);
if (result) {
diff --git a/src/dash-table/components/Filter/Column.tsx b/src/dash-table/components/Filter/Column.tsx
index 5f1aad2ce..1c32da746 100644
--- a/src/dash-table/components/Filter/Column.tsx
+++ b/src/dash-table/components/Filter/Column.tsx
@@ -1,4 +1,4 @@
-import React, { PureComponent } from 'react';
+import React, { CSSProperties, PureComponent } from 'react';
import IsolatedInput from 'core/components/IsolatedInput';
@@ -11,6 +11,7 @@ interface IColumnFilterProps {
columnId: ColumnId;
isValid: boolean;
setFilter: SetFilter;
+ style?: CSSProperties;
value?: string;
}
@@ -36,12 +37,14 @@ export default class ColumnFilter extends PureComponent {
classes,
columnId,
isValid,
+ style,
value
} = this.props;
return (
void;
+type SetFilter = (filter: string, rawFilter: string) => void;
export interface IFilterOptions {
columns: VisibleColumns;
- fillerColumns: number;
+ filter: string;
filtering: Filtering;
- filtering_settings: string;
filtering_type: FilteringType;
id: string;
+ rawFilterQuery: string;
+ row_deletable: boolean;
+ row_selectable: RowSelection;
setFilter: SetFilter;
style_cell: Style;
style_cell_conditional: Cells;
@@ -31,9 +34,11 @@ export interface IFilterOptions {
export default class FilterFactory {
private readonly handlers = new Map();
- private readonly ops = new Map();
private readonly filterStyles = derivedFilterStyles();
private readonly relevantStyles = derivedRelevantFilterStyles();
+ private readonly headerOperations = derivedHeaderOperations();
+
+ private ops = new Map();
private get props() {
return this.propsFn();
@@ -43,139 +48,90 @@ export default class FilterFactory {
}
- private onChange = (columnId: ColumnId, ops: Map, setFilter: SetFilter, ev: any) => {
+ private onChange = (columnId: ColumnId, setFilter: SetFilter, ev: any) => {
Logger.debug('Filter -- onChange', columnId, ev.target.value && ev.target.value.trim());
const value = ev.target.value.trim();
+ const safeColumnId = columnId.toString();
if (value && value.length) {
- ops.set(columnId.toString(), value);
+ this.ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, value));
} else {
- ops.delete(columnId.toString());
+ this.ops.delete(safeColumnId);
}
- setFilter(R.map(
- ([cId, filter]) => `"${cId}" ${filter}`,
- R.filter(
- ([cId]) => this.isFragmentValid(cId),
- Array.from(ops.entries())
- )
- ).join(' && '));
+ const asts = Array.from(this.ops.values());
+ const globalFilter = getMultiColumnQueryString(asts);
+
+ const rawGlobalFilter = R.map(
+ ast => ast.query || '',
+ R.filter(ast => Boolean(ast), asts)
+ ).join(' && ');
+
+ setFilter(globalFilter, rawGlobalFilter);
}
- private getEventHandler = (fn: Function, columnId: ColumnId, ops: Map, setFilter: SetFilter): any => {
+ private getEventHandler = (fn: Function, columnId: ColumnId, setFilter: SetFilter): any => {
const fnHandler = (this.handlers.get(fn) || this.handlers.set(fn, new Map()).get(fn));
const columnIdHandler = (fnHandler.get(columnId) || fnHandler.set(columnId, new Map()).get(columnId));
return (
columnIdHandler.get(setFilter) ||
- (columnIdHandler.set(setFilter, fn.bind(this, columnId, ops, setFilter)).get(setFilter))
+ (columnIdHandler.set(setFilter, fn.bind(this, columnId, setFilter)).get(setFilter))
);
}
- private respectsBasicSyntax(lexemes: ILexemeResult[], allowMultiple: boolean = true) {
- const allowedLexemeTypes = [
- LexemeType.BinaryOperator,
- LexemeType.Expression,
- LexemeType.Operand,
- LexemeType.UnaryOperator
- ];
-
- if (allowMultiple) {
- allowedLexemeTypes.push(LexemeType.And);
- }
-
- const allAllowed = R.all(
- item => R.contains(item.lexeme.name, allowedLexemeTypes),
- lexemes
- );
-
- if (!allAllowed) {
- return false;
- }
-
- const fields = R.map(
- item => item.value,
- R.filter(
- i => i.lexeme.name === LexemeType.Operand,
- lexemes
- )
- );
-
- const uniqueFields = R.uniq(fields);
-
- if (fields.length !== uniqueFields.length) {
- return false;
- }
-
- return true;
- }
+ private updateOps = memoizeOne((query: string) => {
+ const multiQuery = new MultiColumnsSyntaxTree(query);
- private isBasicFilter(
- lexerResult: ILexerResult,
- syntaxerResult: ISyntaxerResult,
- allowMultiple: boolean = true
- ) {
- return lexerResult.valid &&
- syntaxerResult.valid &&
- this.respectsBasicSyntax(lexerResult.lexemes, allowMultiple);
- }
-
- private updateOps(query: string) {
- const lexerResult = lexer(query);
- const syntaxerResult = syntaxer(lexerResult);
-
- if (!this.isBasicFilter(lexerResult, syntaxerResult)) {
+ const newOps = getSingleColumnMap(multiQuery);
+ if (!newOps) {
return;
}
- const { tree } = syntaxerResult;
- if (!tree) {
- this.ops.clear();
- return;
- }
-
- const toCheck: (ISyntaxTree | undefined)[] = [tree];
- while (toCheck.length) {
- const item = toCheck.pop();
- if (!item) {
- continue;
- }
-
- if (item.lexeme.name === LexemeType.UnaryOperator && item.block) {
- this.ops.set(item.block.value, item.value);
- } else if (item.lexeme.name === LexemeType.BinaryOperator && item.left && item.right) {
- this.ops.set(item.left.value, `${item.value} ${item.right.value}`);
- } else {
- toCheck.push(item.left);
- toCheck.push(item.block);
- toCheck.push(item.right);
+ /* Mapping multi-column to single column queries will expand
+ * compressed forms. If the new ast query is equal to the
+ * old one, keep the old one instead.
+ *
+ * If the value was changed by the user, the current ast will
+ * have been modified already and the UI experience will also
+ * be consistent in that case.
+ */
+ R.forEach(([key, ast]) => {
+ const newAst = newOps.get(key);
+
+ if (newAst && newAst.toQueryString() === ast.toQueryString()) {
+ newOps.set(key, ast);
}
- }
- }
-
- private isFragmentValidOrNull(columnId: ColumnId) {
- const op = this.ops.get(columnId.toString());
-
- return !op || !op.trim().length || this.isFragmentValid(columnId);
- }
-
- private isFragmentValid(columnId: ColumnId) {
- const op = this.ops.get(columnId.toString());
-
- const lexerResult = lexer(`"${columnId}" ${op}`);
- const syntaxerResult = syntaxer(lexerResult);
-
- return syntaxerResult.valid && this.isBasicFilter(lexerResult, syntaxerResult, false);
- }
+ }, Array.from(this.ops.entries()));
+
+ this.ops = newOps;
+ });
+
+ private filter = memoizerCache<[ColumnId, number]>()((
+ column: ColumnId,
+ index: number,
+ ast: SingleColumnSyntaxTree | undefined,
+ setFilter: SetFilter
+ ) => {
+ return ();
+ });
public createFilters() {
const {
columns,
- fillerColumns,
+ filter,
filtering,
- filtering_settings,
filtering_type,
+ row_deletable,
+ row_selectable,
setFilter,
style_cell,
style_cell_conditional,
@@ -187,7 +143,7 @@ export default class FilterFactory {
return [];
}
- this.updateOps(filtering_settings);
+ this.updateOps(filter);
if (filtering_type === FilteringType.Basic) {
const filterStyles = this.relevantStyles(
@@ -203,14 +159,12 @@ export default class FilterFactory {
);
const filters = R.addIndex(R.map)((column, index) => {
- return ();
+ return this.filter.get(column.id, index)(
+ column.id,
+ index,
+ this.ops.get(column.id.toString()),
+ setFilter
+ );
}, columns);
const styledFilters = arrayMap(
@@ -218,9 +172,13 @@ export default class FilterFactory {
wrapperStyles,
(f, s) => React.cloneElement(f, { style: s }));
- const offsets = R.range(0, fillerColumns).map(i => ( | ));
+ const operations = this.headerOperations(
+ 1,
+ row_selectable,
+ row_deletable
+ )[0];
- return [offsets.concat(styledFilters)];
+ return [operations.concat(styledFilters)];
} else {
return [[]];
}
diff --git a/src/dash-table/components/Table/index.tsx b/src/dash-table/components/Table/index.tsx
index db9eadacb..1ce9b95a2 100644
--- a/src/dash-table/components/Table/index.tsx
+++ b/src/dash-table/components/Table/index.tsx
@@ -12,6 +12,8 @@ import derivedVirtualData from 'dash-table/derived/data/virtual';
import derivedVirtualizedData from 'dash-table/derived/data/virtualized';
import derivedVisibleColumns from 'dash-table/derived/column/visible';
+import QuerySyntaxTree from 'dash-table/syntax-tree/QuerySyntaxTree';
+
import {
ControlledTableProps,
PropsWithDefaultsAndDerived,
@@ -37,6 +39,7 @@ export default class Table extends Component = {};
+ if (!derivedStructureCache.cached) {
+ newProps.derived_filter_structure = derivedStructureCache.result;
+ }
+
if (!virtualCached) {
newProps.derived_virtual_data = virtual.data;
newProps.derived_virtual_indices = virtual.indices;
@@ -260,4 +269,7 @@ export default class Table extends Component viewport);
private readonly virtualCache = memoizeOneWithFlag(virtual => virtual);
private readonly virtualSelectedRowsCache = memoizeOneWithFlag(virtual => virtual);
+ private readonly structuredQueryCache = memoizeOneWithFlag(
+ (query: string) => new QuerySyntaxTree(query).toStructure()
+ );
}
\ No newline at end of file
diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts
index d8b42501a..1a8f1e64d 100644
--- a/src/dash-table/components/Table/props.ts
+++ b/src/dash-table/components/Table/props.ts
@@ -232,6 +232,7 @@ export interface IUSerInterfaceTooltip {
export interface IState {
forcedResizeOnly: boolean;
+ rawFilterQuery: string;
scrollbarWidth: number;
tooltip?: IUSerInterfaceTooltip;
uiViewport?: IUserInterfaceViewport;
@@ -265,8 +266,8 @@ interface IProps {
data?: Data;
dropdown_properties: any; // legacy
editable?: boolean;
+ filter?: string;
filtering?: Filtering;
- filtering_settings?: string;
filtering_type?: FilteringType;
filtering_types?: FilteringType[];
locale_format: INumberLocale;
@@ -308,8 +309,8 @@ interface IDefaultProps {
css: IStylesheetRule[];
data: Data;
editable: boolean;
+ filter: string;
filtering: Filtering;
- filtering_settings: string;
filtering_type: FilteringType;
filtering_types: FilteringType[];
merge_duplicate_headers: boolean;
@@ -344,6 +345,7 @@ interface IDefaultProps {
}
interface IDerivedProps {
+ derived_filter_structure: object | null;
derived_viewport_data: Data;
derived_viewport_indices: Indices;
derived_viewport_selected_rows: Indices;
diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts
index b8173235c..f575435e4 100644
--- a/src/dash-table/conditional/index.ts
+++ b/src/dash-table/conditional/index.ts
@@ -1,5 +1,5 @@
import { ColumnId, Datum, ColumnType } from 'dash-table/components/Table/props';
-import SyntaxTree from 'core/syntax-tree';
+import { QuerySyntaxTree } from 'dash-table/syntax-tree';
export interface IConditionalElement {
filter?: string;
@@ -26,7 +26,7 @@ export type ConditionalDataCell = IConditionalElement & IIndexedRowElement & INa
export type ConditionalCell = INamedElement & ITypedElement;
export type ConditionalHeader = IIndexedHeaderElement & INamedElement & ITypedElement;
-function ifAstFilter(ast: SyntaxTree, datum: Datum) {
+function ifAstFilter(ast: QuerySyntaxTree, datum: Datum) {
return ast.isValid && ast.evaluate(datum);
}
@@ -69,5 +69,5 @@ export function ifHeaderIndex(condition: IIndexedHeaderElement | undefined, head
export function ifFilter(condition: IConditionalElement | undefined, datum: Datum) {
return !condition ||
condition.filter === undefined ||
- ifAstFilter(new SyntaxTree(condition.filter), datum);
+ ifAstFilter(new QuerySyntaxTree(condition.filter), datum);
}
\ No newline at end of file
diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js
index 5be2d8c53..b0ab6b87b 100644
--- a/src/dash-table/dash/DataTable.js
+++ b/src/dash-table/dash/DataTable.js
@@ -40,8 +40,8 @@ export const defaultProps = {
content_style: 'grow',
css: [],
+ filter: '',
filtering: false,
- filtering_settings: '',
filtering_type: 'basic',
filtering_types: ['basic'],
sorting: false,
@@ -771,6 +771,16 @@ export const propTypes = {
*/
tooltip_duration: PropTypes.number,
+ /**
+ * If `filtering` is enabled, then the current filtering
+ * string is represented in this `filter`
+ * property.
+ * NOTE: The shape and structure of this property will
+ * likely change in the future.
+ * Stay tuned in [https://github.com/plotly/dash-table/issues/169](https://github.com/plotly/dash-table/issues/169)
+ */
+ filter: PropTypes.string,
+
/**
* The `filtering` property controls the behavior of the `filtering` UI.
* If `False`, then the filtering UI is not displayed
@@ -779,7 +789,7 @@ export const propTypes = {
* that exists in the `data` property.
* If `be`, then the filtering UI is displayed but it is the
* responsibility of the developer to program the filtering
- * through a callback (where `filtering_settings` would be the input
+ * through a callback (where `filter` would be the input
* and `data` would be the output).
*
* NOTE - Several aspects of filtering may change in the future,
@@ -788,16 +798,6 @@ export const propTypes = {
*/
filtering: PropTypes.oneOf(['fe', 'be', true, false]),
- /**
- * If `filtering` is enabled, then the current filtering
- * string is represented in this `filtering_settings`
- * property.
- * NOTE: The shape and structure of this property will
- * likely change in the future.
- * Stay tuned in [https://github.com/plotly/dash-table/issues/169](https://github.com/plotly/dash-table/issues/169)
- */
- filtering_settings: PropTypes.string,
-
/**
* UNSTABLE
* In the future, there may be several modes of the
@@ -982,6 +982,38 @@ export const propTypes = {
*/
virtualization: PropTypes.bool,
+ /**
+ * This property represents the current structure of
+ * `filter` as a tree structure. Each node of the
+ * query structure have:
+ * - type (string; required)
+ * - 'open-block'
+ * - 'logical-operator'
+ * - 'relational-operator'
+ * - 'unary-operator'
+ * - 'expression'
+ * - 'operand'
+ * - subType (string; optional)
+ * - 'open-block': '()'
+ * - 'logical-operator': '&&', '||'
+ * - 'relational-operator': '=', '>=', '>', '<=', '<', '!=', 'contains'
+ * - 'unary-operator': '!', 'is bool', 'is even', 'is nil', 'is num', 'is object', 'is odd', 'is prime', 'is str'
+ * - 'expression': 'value', 'field'
+ * - 'operand': 'field'
+ * - value (any)
+ * - 'expression, value': passed value
+ * - 'expression, field': the field/prop name
+ * - 'operand, field': the field/prop name
+ *
+ * - block (nested query structure; optional)
+ * - left (nested query structure; optional)
+ * - right (nested query structure; optional)
+ *
+ * If the query is invalid or empty, the `derived_filter_structure` will
+ * be null.
+ */
+ derived_filter_structure: PropTypes.object,
+
/**
* This property represents the current state of `data`
* on the current page. This property will be updated
diff --git a/src/dash-table/derived/cell/dropdowns.ts b/src/dash-table/derived/cell/dropdowns.ts
index 06884658e..2f8e81ef1 100644
--- a/src/dash-table/derived/cell/dropdowns.ts
+++ b/src/dash-table/derived/cell/dropdowns.ts
@@ -2,7 +2,6 @@ import * as R from 'ramda';
import { memoizeOne } from 'core/memoizer';
import memoizerCache from 'core/cache/memoizer';
-import SyntaxTree from 'core/syntax-tree';
import {
IConditionalDropdown
@@ -18,6 +17,7 @@ import {
IBaseVisibleColumn,
IVisibleColumn
} from 'dash-table/components/Table/props';
+import { QuerySyntaxTree } from 'dash-table/syntax-tree';
const mapData = R.addIndex(R.map);
@@ -116,13 +116,13 @@ class Dropdowns {
*/
private readonly ast = memoizerCache<[ColumnId, number]>()((
query: string
- ) => new SyntaxTree(query));
+ ) => new QuerySyntaxTree(query));
/**
* Evaluate if the query matches the cell's data.
*/
private readonly evaluation = memoizerCache<[ColumnId, number]>()((
- ast: SyntaxTree,
+ ast: QuerySyntaxTree,
datum: Datum
) => ast.evaluate(datum));
}
\ No newline at end of file
diff --git a/src/dash-table/derived/data/virtual.ts b/src/dash-table/derived/data/virtual.ts
index cebccb1b4..f46b597f1 100644
--- a/src/dash-table/derived/data/virtual.ts
+++ b/src/dash-table/derived/data/virtual.ts
@@ -2,7 +2,6 @@ import * as R from 'ramda';
import { memoizeOneFactory } from 'core/memoizer';
import sort, { defaultIsNully, SortSettings } from 'core/sorting';
-import SyntaxTree from 'core/syntax-tree';
import {
Data,
Datum,
@@ -10,11 +9,12 @@ import {
IDerivedData,
Sorting
} from 'dash-table/components/Table/props';
+import { QuerySyntaxTree } from 'dash-table/syntax-tree';
const getter = (
data: Data,
filtering: Filtering,
- filtering_settings: string,
+ filter: string,
sorting: Sorting,
sorting_settings: SortSettings = [],
sorting_treat_empty_string_as_none: boolean
@@ -25,7 +25,7 @@ const getter = (
}, data);
if (filtering === 'fe' || filtering === true) {
- const tree = new SyntaxTree(filtering_settings);
+ const tree = new QuerySyntaxTree(filter);
data = tree.isValid ?
tree.filter(data) :
diff --git a/src/dash-table/derived/style/index.ts b/src/dash-table/derived/style/index.ts
index d73ad6008..641671bea 100644
--- a/src/dash-table/derived/style/index.ts
+++ b/src/dash-table/derived/style/index.ts
@@ -1,7 +1,6 @@
import * as R from 'ramda';
import { CSSProperties } from 'react';
-import SyntaxTree from 'core/syntax-tree';
import { memoizeOneFactory } from 'core/memoizer';
import { Datum, IVisibleColumn } from 'dash-table/components/Table/props';
@@ -25,6 +24,7 @@ import {
ifColumnId,
ifColumnType
} from 'dash-table/conditional';
+import { QuerySyntaxTree } from 'dash-table/syntax-tree';
export interface IConvertedStyle {
style: CSSProperties;
@@ -38,7 +38,7 @@ type GenericStyle = Style & Partial<{ if: GenericIf }>;
function convertElement(style: GenericStyle) {
const indexFilter = style.if && (style.if.header_index || style.if.row_index);
- let ast: SyntaxTree;
+ let ast: QuerySyntaxTree;
return {
matchesColumn: (column: IVisibleColumn) =>
@@ -55,7 +55,7 @@ function convertElement(style: GenericStyle) {
matchesFilter: (datum: Datum) =>
!style.if ||
style.if.filter === undefined ||
- (ast = ast || new SyntaxTree(style.if.filter)).evaluate(datum),
+ (ast = ast || new QuerySyntaxTree(style.if.filter)).evaluate(datum),
style: convertStyle(style)
};
}
diff --git a/src/dash-table/derived/table/index.tsx b/src/dash-table/derived/table/index.tsx
index 3e00f8045..2aa87abf2 100644
--- a/src/dash-table/derived/table/index.tsx
+++ b/src/dash-table/derived/table/index.tsx
@@ -1,43 +1,21 @@
+import * as R from 'ramda';
+
+import { memoizeOne } from 'core/memoizer';
+
import CellFactory from 'dash-table/components/CellFactory';
import FilterFactory from 'dash-table/components/FilterFactory';
import HeaderFactory from 'dash-table/components/HeaderFactory';
-import { ControlledTableProps, SetProps } from 'dash-table/components/Table/props';
-
-const handleSetFilter = (setProps: SetProps, filtering_settings: string) => setProps({ filtering_settings });
+import { ControlledTableProps, SetProps, SetState } from 'dash-table/components/Table/props';
-function filterPropsFn(propsFn: () => ControlledTableProps) {
- const {
- columns,
- filtering,
- filtering_settings,
- filtering_type,
- id,
- row_deletable,
- row_selectable,
- setProps,
- style_cell,
- style_cell_conditional,
- style_filter,
- style_filter_conditional
- } = propsFn();
+const handleSetFilter = (setProps: SetProps, setState: SetState, filter: string, rawFilterQuery: string) => {
+ setProps({ filter });
+ setState({ rawFilterQuery });
+};
- const fillerColumns =
- (row_deletable ? 1 : 0) +
- (row_selectable ? 1 : 0);
+function filterPropsFn(propsFn: () => ControlledTableProps, setFilter: any) {
+ const props = propsFn();
- return {
- columns,
- fillerColumns,
- filtering,
- filtering_settings,
- filtering_type,
- id,
- setFilter: handleSetFilter.bind(undefined, setProps),
- style_cell,
- style_cell_conditional,
- style_filter,
- style_filter_conditional
- };
+ return R.merge(props, { setFilter });
}
function getter(
@@ -59,8 +37,17 @@ function getter(
}
export default (propsFn: () => ControlledTableProps) => {
+ const setFilter = memoizeOne((
+ setProps: SetProps,
+ setState: SetState
+ ) => handleSetFilter.bind(undefined, setProps, setState));
+
const cellFactory = new CellFactory(propsFn);
- const filterFactory = new FilterFactory(() => filterPropsFn(propsFn));
+ const filterFactory = new FilterFactory(() => {
+ const props = propsFn();
+
+ return filterPropsFn(propsFn, setFilter(props.setProps, props.setState));
+ });
const headerFactory = new HeaderFactory(propsFn);
return getter.bind(undefined, cellFactory, filterFactory, headerFactory);
diff --git a/src/dash-table/syntax-tree/MultiColumnsSyntaxTree.ts b/src/dash-table/syntax-tree/MultiColumnsSyntaxTree.ts
new file mode 100644
index 000000000..e492e1576
--- /dev/null
+++ b/src/dash-table/syntax-tree/MultiColumnsSyntaxTree.ts
@@ -0,0 +1,46 @@
+import * as R from 'ramda';
+
+import SyntaxTree from 'core/syntax-tree';
+import { LexemeType } from 'core/syntax-tree/lexicon';
+import { ISyntaxTree } from 'core/syntax-tree/syntaxer';
+
+import columnMultiLexicon from './lexicon/columnMulti';
+
+export default class MultiColumnsSyntaxTree extends SyntaxTree {
+ constructor(query: string) {
+ super(columnMultiLexicon, query);
+ }
+ get isValid() {
+ return super.isValid &&
+ this.respectsBasicSyntax();
+ }
+ get statements() {
+ if (!this.syntaxerResult.tree) {
+ return;
+ }
+ const statements: ISyntaxTree[] = [];
+ const toCheck: ISyntaxTree[] = [this.syntaxerResult.tree];
+ while (toCheck.length) {
+ const item = toCheck.pop();
+ if (!item) {
+ continue;
+ }
+ statements.push(item);
+ if (item.left) {
+ toCheck.push(item.left);
+ }
+ if (item.block) {
+ toCheck.push(item.block);
+ }
+ if (item.right) {
+ toCheck.push(item.right);
+ }
+ }
+ return statements;
+ }
+ private respectsBasicSyntax() {
+ const fields = R.map(item => item.value, R.filter(i => i.lexeme.type === LexemeType.Operand, this.lexerResult.lexemes));
+ const uniqueFields = R.uniq(fields);
+ return fields.length === uniqueFields.length;
+ }
+}
\ No newline at end of file
diff --git a/src/dash-table/syntax-tree/QuerySyntaxTree.ts b/src/dash-table/syntax-tree/QuerySyntaxTree.ts
new file mode 100644
index 000000000..a309b9d2d
--- /dev/null
+++ b/src/dash-table/syntax-tree/QuerySyntaxTree.ts
@@ -0,0 +1,9 @@
+import SyntaxTree from 'core/syntax-tree';
+
+import queryLexicon from './lexicon/query';
+
+export default class QuerySyntaxTree extends SyntaxTree {
+ constructor(query: string) {
+ super(queryLexicon, query);
+ }
+}
\ No newline at end of file
diff --git a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts
new file mode 100644
index 000000000..a2d2b7076
--- /dev/null
+++ b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts
@@ -0,0 +1,51 @@
+import SyntaxTree from 'core/syntax-tree';
+import { ILexemeResult, ILexerResult } from 'core/syntax-tree/lexer';
+import { LexemeType, boundLexeme } from 'core/syntax-tree/lexicon';
+
+import { ColumnId } from 'dash-table/components/Table/props';
+
+import operand from './lexeme/operand';
+import { equal } from './lexeme/relational';
+
+import columnLexicon from './lexicon/column';
+
+function isBinary(lexemes: ILexemeResult[]) {
+ return lexemes.length === 2;
+}
+
+function isExpression(lexemes: ILexemeResult[]) {
+ return lexemes.length === 1 &&
+ lexemes[0].lexeme.type === LexemeType.Expression;
+}
+
+function isUnary(lexemes: ILexemeResult[]) {
+ return lexemes.length === 1 &&
+ lexemes[0].lexeme.type === LexemeType.UnaryOperator;
+}
+
+export function modifyLex(key: ColumnId, res: ILexerResult) {
+ if (!res.valid) {
+ return res;
+ }
+
+ if (isBinary(res.lexemes) || isUnary(res.lexemes)) {
+ res.lexemes = [
+ { lexeme: boundLexeme(operand), value: `{${key}}` },
+ ...res.lexemes
+ ];
+ } else if (isExpression(res.lexemes)) {
+ res.lexemes = [
+ { lexeme: boundLexeme(operand), value: `{${key}}` },
+ { lexeme: boundLexeme(equal), value: 'eq' },
+ ...res.lexemes
+ ];
+ }
+
+ return res;
+}
+
+export default class SingleColumnSyntaxTree extends SyntaxTree {
+ constructor(key: ColumnId, query: string) {
+ super(columnLexicon, query, modifyLex.bind(undefined, key));
+ }
+}
\ No newline at end of file
diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts
new file mode 100644
index 000000000..a651fbef0
--- /dev/null
+++ b/src/dash-table/syntax-tree/index.ts
@@ -0,0 +1,47 @@
+import * as R from 'ramda';
+
+import { LexemeType } from 'core/syntax-tree/lexicon';
+
+import MultiColumnsSyntaxTree from './MultiColumnsSyntaxTree';
+import QuerySyntaxTree from './QuerySyntaxTree';
+import SingleColumnSyntaxTree from './SingleColumnSyntaxTree';
+import { RelationalOperator } from './lexeme/relational';
+
+export const getMultiColumnQueryString = (
+ asts: SingleColumnSyntaxTree[]
+) => R.map(
+ ast => ast.toQueryString(),
+ R.filter(ast => ast && ast.isValid, asts)
+ ).join(' && ');
+
+export const getSingleColumnMap = (ast: MultiColumnsSyntaxTree) => {
+ if (!ast.isValid) {
+ return;
+ }
+
+ const map = new Map();
+
+ const statements = ast.statements;
+ if (!statements) {
+ return map;
+ }
+
+ R.forEach(s => {
+ if (s.lexeme.type === LexemeType.UnaryOperator && s.left) {
+ const sanitizedColumnId = s.left.lexeme.present ? s.left.lexeme.present(s.left) : s.left.value;
+ map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, s.value));
+ } else if (s.lexeme.type === LexemeType.RelationalOperator && s.left && s.right) {
+ const sanitizedColumnId = s.left.lexeme.present ? s.left.lexeme.present(s.left) : s.left.value;
+
+ if (s.lexeme.present && s.lexeme.present(s) === RelationalOperator.Equal) {
+ map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, `${s.right.value}`));
+ } else {
+ map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, `${s.value} ${s.right.value}`));
+ }
+ }
+ }, statements);
+
+ return map;
+};
+
+export { MultiColumnsSyntaxTree, QuerySyntaxTree, SingleColumnSyntaxTree };
\ No newline at end of file
diff --git a/src/dash-table/syntax-tree/lexeme/block.ts b/src/dash-table/syntax-tree/lexeme/block.ts
new file mode 100644
index 000000000..3e804ba87
--- /dev/null
+++ b/src/dash-table/syntax-tree/lexeme/block.ts
@@ -0,0 +1,28 @@
+import Logger from 'core/Logger';
+import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon';
+
+export const blockClose: IUnboundedLexeme = {
+ nesting: -1,
+ regexp: /^\)/,
+ type: LexemeType.BlockClose
+};
+
+export const blockOpen: IUnboundedLexeme = {
+ evaluate: (target, tree) => {
+ Logger.trace('evaluate -> ()', target, tree);
+
+ const t = tree as any;
+
+ return t.block.lexeme.evaluate(target, t.block);
+ },
+ type: LexemeType.BlockOpen,
+ nesting: 1,
+ subType: '()',
+ priority: 1,
+ regexp: /^\(/,
+ syntaxer: (lexs: any[]) => {
+ return Object.assign({
+ block: lexs.slice(1, lexs.length - 1)
+ }, lexs[0]);
+ }
+};
\ No newline at end of file
diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts
new file mode 100644
index 000000000..8dad633ee
--- /dev/null
+++ b/src/dash-table/syntax-tree/lexeme/expression.ts
@@ -0,0 +1,59 @@
+import isNumeric from 'fast-isnumeric';
+import * as R from 'ramda';
+
+import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon';
+import { ISyntaxTree } from 'core/syntax-tree/syntaxer';
+import operand from './operand';
+
+const STRING_REGEX = /^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`))/;
+const VALUE_REGEX = /^(([^\s'"`{}()\\]|\\.)+)(?:[\s)]|$)/;
+
+export const fieldExpression: IUnboundedLexeme = R.merge(
+ operand, {
+ subType: 'field',
+ type: LexemeType.Expression
+ }
+);
+
+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) => {
+ if (STRING_REGEX.test(tree.value)) {
+ return getString(tree.value);
+ } else {
+ throw new Error();
+ }
+ },
+ regexp: STRING_REGEX,
+ subType: 'value',
+ 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
diff --git a/src/dash-table/syntax-tree/lexeme/logical.ts b/src/dash-table/syntax-tree/lexeme/logical.ts
new file mode 100644
index 000000000..3fb4a0df3
--- /dev/null
+++ b/src/dash-table/syntax-tree/lexeme/logical.ts
@@ -0,0 +1,49 @@
+import Logger from 'core/Logger';
+import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon';
+
+enum LogicalOperator {
+ And = '&&',
+ Or = '||'
+}
+
+export const and: IUnboundedLexeme = {
+ evaluate: (target, tree) => {
+ Logger.trace('evaluate -> &&', target, tree);
+
+ const t = tree as any;
+ const lv = t.left.lexeme.evaluate(target, t.left);
+ const rv = t.right.lexeme.evaluate(target, t.right);
+ return lv && rv;
+ },
+ type: LexemeType.LogicalOperator,
+ priority: 2,
+ regexp: /^(and\s|&&)/i,
+ subType: LogicalOperator.And,
+ syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => {
+ return Object.assign({
+ left: lexs.slice(0, pivotIndex),
+ right: lexs.slice(pivotIndex + 1)
+ }, pivot);
+ }
+};
+
+export const or: IUnboundedLexeme = {
+ evaluate: (target, tree) => {
+ Logger.trace('evaluate -> ||', target, tree);
+
+ const t = tree as any;
+
+ return t.left.lexeme.evaluate(target, t.left) ||
+ t.right.lexeme.evaluate(target, t.right);
+ },
+ type: LexemeType.LogicalOperator,
+ subType: LogicalOperator.Or,
+ priority: 3,
+ regexp: /^(or\s|\|\|)/i,
+ syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => {
+ return Object.assign({
+ left: lexs.slice(0, pivotIndex),
+ right: lexs.slice(pivotIndex + 1)
+ }, pivot);
+ }
+};
\ No newline at end of file
diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts
new file mode 100644
index 000000000..5fe72feea
--- /dev/null
+++ b/src/dash-table/syntax-tree/lexeme/operand.ts
@@ -0,0 +1,26 @@
+import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon';
+import { ISyntaxTree } from 'core/syntax-tree/syntaxer';
+
+const FIELD_REGEX = /^{(([^{}\\]|\\.)+)}/;
+
+const getField = (
+ value: string
+) => value
+ .slice(1, value.length - 1)
+ .replace(/\\(.)/g, '$1');
+
+const operand: IUnboundedLexeme = {
+ present: (tree: ISyntaxTree) => getField(tree.value),
+ resolve: (target: any, tree: ISyntaxTree) => {
+ if (FIELD_REGEX.test(tree.value)) {
+ return target[getField(tree.value)];
+ } else {
+ throw new Error();
+ }
+ },
+ regexp: FIELD_REGEX,
+ subType: 'field',
+ type: LexemeType.Operand
+};
+
+export default operand;
\ No newline at end of file
diff --git a/src/dash-table/syntax-tree/lexeme/relational.ts b/src/dash-table/syntax-tree/lexeme/relational.ts
new file mode 100644
index 000000000..df6fb9a78
--- /dev/null
+++ b/src/dash-table/syntax-tree/lexeme/relational.ts
@@ -0,0 +1,92 @@
+import * as R from 'ramda';
+
+import Logger from 'core/Logger';
+import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon';
+import { ISyntaxTree } from 'core/syntax-tree/syntaxer';
+
+function evaluator(
+ target: any,
+ tree: ISyntaxTree
+): [any, any] {
+ Logger.trace('evaluate -> relational', target, tree);
+
+ const t = tree as any;
+
+ const opValue = t.left.lexeme.resolve(target, t.left);
+ const expValue = t.right.lexeme.resolve(target, t.right);
+ Logger.trace(`opValue: ${opValue}, expValue: ${expValue}`);
+
+ return [opValue, expValue];
+}
+
+function relationalSyntaxer([left, lexeme, right]: any[]) {
+ return Object.assign({ left, right }, lexeme);
+}
+
+function relationalEvaluator(
+ fn: ([opValue, expValue]: any[]) => boolean
+) {
+ return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree));
+}
+
+export enum RelationalOperator {
+ Contains = 'contains',
+ Equal = '=',
+ GreaterOrEqual = '>=',
+ GreaterThan = '>',
+ LessOrEqual = '<=',
+ LessThan = '<',
+ NotEqual = '!='
+}
+
+const LEXEME_BASE = {
+ priority: 0,
+ syntaxer: relationalSyntaxer,
+ type: LexemeType.RelationalOperator
+};
+
+export const contains: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(([op, exp]) =>
+ typeof op === 'string' &&
+ typeof exp === 'string' &&
+ op.indexOf(exp) !== -1
+ ),
+ subType: RelationalOperator.Contains,
+ regexp: /^(contains)/i
+}, LEXEME_BASE);
+
+export const equal: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(([op, exp]) => op === exp),
+ subType: RelationalOperator.Equal,
+ regexp: /^(=|eq)/i
+}, LEXEME_BASE);
+
+export const greaterOrEqual: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(([op, exp]) => op >= exp),
+ subType: RelationalOperator.GreaterOrEqual,
+ regexp: /^(>=|ge)/i
+}, LEXEME_BASE);
+
+export const greaterThan: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(([op, exp]) => op > exp),
+ subType: RelationalOperator.GreaterThan,
+ regexp: /^(>|gt)/i
+}, LEXEME_BASE);
+
+export const lessOrEqual: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(([op, exp]) => op <= exp),
+ subType: RelationalOperator.LessOrEqual,
+ regexp: /^(<=|le)/i
+}, LEXEME_BASE);
+
+export const lessThan: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(([op, exp]) => op < exp),
+ subType: RelationalOperator.LessThan,
+ regexp: /^(<|lt)/i
+}, LEXEME_BASE);
+
+export const notEqual: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(([op, exp]) => op !== exp),
+ subType: RelationalOperator.NotEqual,
+ regexp: /^(!=|ne)/i
+}, LEXEME_BASE);
\ No newline at end of file
diff --git a/src/dash-table/syntax-tree/lexeme/unary.ts b/src/dash-table/syntax-tree/lexeme/unary.ts
new file mode 100644
index 000000000..aab1d5f52
--- /dev/null
+++ b/src/dash-table/syntax-tree/lexeme/unary.ts
@@ -0,0 +1,106 @@
+import * as R from 'ramda';
+
+import Logger from 'core/Logger';
+import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon';
+import { ISyntaxTree } from 'core/syntax-tree/syntaxer';
+
+const checkPrimality = (c: number) => {
+ if (c === 2) { return true; }
+ if (c < 2 || c % 2 === 0) { return false; }
+ for (let n = 3; n * n <= c; n += 2) { if (c % n === 0) { return false; } }
+ return true;
+};
+
+function evaluator(
+ target: any,
+ tree: ISyntaxTree
+): any {
+ Logger.trace('evaluate -> unary', target, tree);
+
+ Logger.trace('evaluate -> unary', target, tree);
+
+ const t = tree as any;
+ const opValue = t.left.lexeme.resolve(target, t.left);
+
+ return opValue;
+}
+
+function relationalSyntaxer([left, lexeme]: any[]) {
+ return Object.assign({ left }, lexeme);
+}
+
+function relationalEvaluator(
+ fn: (opValue: any[]) => boolean
+) {
+ return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree));
+}
+
+enum UnaryOperator {
+ Not = '!'
+}
+
+const LEXEME_BASE = {
+ present: (tree: ISyntaxTree) => tree.value,
+ priority: 0,
+ syntaxer: relationalSyntaxer,
+ type: LexemeType.UnaryOperator
+};
+
+export const not: IUnboundedLexeme = {
+ evaluate: (target, tree) => {
+ Logger.trace('evaluate -> unary not', target, tree);
+
+ const t = tree as any;
+
+ return !t.right.lexeme.evaluate(target, t.right);
+ },
+ type: LexemeType.UnaryOperator,
+ subType: UnaryOperator.Not,
+ priority: 1.5,
+ regexp: /^!/,
+ syntaxer: (lexs: any[]) => {
+ return Object.assign({
+ right: lexs.slice(1, lexs.length)
+ }, lexs[0]);
+ }
+};
+
+export const isBool: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(opValue => typeof opValue === 'boolean'),
+ regexp: /^(is bool)/i
+}, LEXEME_BASE);
+
+export const isEven: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(opValue => typeof opValue === 'number' && opValue % 2 === 0),
+ regexp: /^(is even)/i
+}, LEXEME_BASE);
+
+export const isNil: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(opValue => opValue === undefined || opValue === null),
+ regexp: /^(is nil)/i
+}, LEXEME_BASE);
+
+export const isNum: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(opValue => typeof opValue === 'number'),
+ regexp: /^(is num)/i
+}, LEXEME_BASE);
+
+export const isObject: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(opValue => opValue !== null && typeof opValue === 'object'),
+ regexp: /^(is object)/i
+}, LEXEME_BASE);
+
+export const isOdd: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(opValue => typeof opValue === 'number' && opValue % 2 === 1),
+ regexp: /^(is odd)/i
+}, LEXEME_BASE);
+
+export const isPrime: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(opValue => typeof opValue === 'number' && checkPrimality(opValue)),
+ regexp: /^(is prime)/i
+}, LEXEME_BASE);
+
+export const isStr: IUnboundedLexeme = R.merge({
+ evaluate: relationalEvaluator(opValue => typeof opValue === 'string'),
+ regexp: /^(is str)/i
+}, LEXEME_BASE);
\ 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
new file mode 100644
index 000000000..b6e42ec6d
--- /dev/null
+++ b/src/dash-table/syntax-tree/lexicon/column.ts
@@ -0,0 +1,72 @@
+import * as R from 'ramda';
+
+import {
+ fieldExpression,
+ stringExpression,
+ valueExpression
+} from '../lexeme/expression';
+import {
+ contains,
+ equal,
+ greaterOrEqual,
+ greaterThan,
+ lessOrEqual,
+ lessThan,
+ notEqual
+} from '../lexeme/relational';
+import {
+ isBool,
+ isEven,
+ isNil,
+ isNum,
+ isObject,
+ isOdd,
+ isPrime,
+ isStr
+} from '../lexeme/unary';
+
+import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon';
+import { ILexemeResult } from 'core/syntax-tree/lexer';
+
+const lexicon: ILexeme[] = [
+ ...[contains,
+ equal,
+ greaterOrEqual,
+ greaterThan,
+ lessOrEqual,
+ lessThan,
+ notEqual
+ ].map(op => ({
+ ...op,
+ terminal: false,
+ if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous
+ })),
+ ...[isBool,
+ isEven,
+ isNil,
+ isNum,
+ isObject,
+ isOdd,
+ isPrime,
+ isStr
+ ].map(op => ({
+ ...op,
+ if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous,
+ terminal: true
+ })),
+ ...[
+ fieldExpression,
+ stringExpression,
+ valueExpression
+ ].map(exp => ({
+ ...exp,
+ if: (_lexs: ILexemeResult[], previous: ILexemeResult) =>
+ !previous || R.contains(
+ previous.lexeme.type,
+ [LexemeType.RelationalOperator]
+ ),
+ terminal: true
+ }))
+];
+
+export default lexicon;
\ No newline at end of file
diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts
new file mode 100644
index 000000000..a8542b5bc
--- /dev/null
+++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts
@@ -0,0 +1,105 @@
+import * as R from 'ramda';
+
+import {
+ fieldExpression,
+ stringExpression,
+ valueExpression
+} from '../lexeme/expression';
+import {
+ and
+} from '../lexeme/logical';
+import operand from '../lexeme/operand';
+import {
+ contains,
+ equal,
+ greaterOrEqual,
+ greaterThan,
+ lessOrEqual,
+ lessThan,
+ notEqual
+} from '../lexeme/relational';
+import {
+ isBool,
+ isEven,
+ isNil,
+ isNum,
+ isObject,
+ isOdd,
+ isPrime,
+ isStr
+} from '../lexeme/unary';
+
+import { ILexeme, LexemeType } from 'core/syntax-tree/lexicon';
+import { ILexemeResult } from 'core/syntax-tree/lexer';
+
+const lexicon: ILexeme[] = [
+ {
+ ...and,
+ if: (_: ILexemeResult[], previous: ILexemeResult) =>
+ previous && R.contains(
+ previous.lexeme.type,
+ [
+ LexemeType.Expression,
+ LexemeType.UnaryOperator
+ ]
+ ),
+ terminal: false
+ },
+ {
+ ...operand,
+ if: (_: ILexemeResult[], previous: ILexemeResult) =>
+ !previous || R.contains(
+ previous.lexeme.type,
+ [LexemeType.LogicalOperator]
+ ),
+ terminal: false
+ },
+ ...[contains,
+ equal,
+ greaterOrEqual,
+ greaterThan,
+ lessOrEqual,
+ lessThan,
+ notEqual
+ ].map(op => ({
+ ...op,
+ if: (_: ILexemeResult[], previous: ILexemeResult) =>
+ previous && R.contains(
+ previous.lexeme.type,
+ [LexemeType.Operand]
+ ),
+ terminal: false
+ })),
+ ...[isBool,
+ isEven,
+ isNil,
+ isNum,
+ isObject,
+ isOdd,
+ isPrime,
+ isStr
+ ].map(op => ({
+ ...op,
+ if: (_: ILexemeResult[], previous: ILexemeResult) =>
+ previous && R.contains(
+ previous.lexeme.type,
+ [LexemeType.Operand]
+ ),
+ terminal: true
+ })),
+ ...[
+ fieldExpression,
+ stringExpression,
+ valueExpression
+ ].map(exp => ({
+ ...exp,
+ if: (_: ILexemeResult[], previous: ILexemeResult) =>
+ previous && R.contains(
+ previous.lexeme.type,
+ [LexemeType.RelationalOperator]
+ ),
+ terminal: true
+ }))
+];
+
+export default lexicon;
\ No newline at end of file
diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts
new file mode 100644
index 000000000..8b23c1b63
--- /dev/null
+++ b/src/dash-table/syntax-tree/lexicon/query.ts
@@ -0,0 +1,168 @@
+import * as R from 'ramda';
+
+import {
+ blockClose,
+ blockOpen
+} from '../lexeme/block';
+import {
+ fieldExpression,
+ stringExpression,
+ valueExpression
+} from '../lexeme/expression';
+import {
+ and,
+ or
+} from '../lexeme/logical';
+import operand from '../lexeme/operand';
+import {
+ contains,
+ equal,
+ greaterOrEqual,
+ greaterThan,
+ lessOrEqual,
+ lessThan,
+ notEqual
+} from '../lexeme/relational';
+import {
+ isBool,
+ isEven,
+ isNil,
+ isNum,
+ isObject,
+ isOdd,
+ isPrime,
+ isStr,
+ not
+} from '../lexeme/unary';
+
+import { ILexemeResult } from 'core/syntax-tree/lexer';
+import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon';
+
+const nestingReducer = R.reduce(
+ (nesting, l) => nesting + (l.lexeme.nesting || 0)
+);
+
+const isTerminal = (lexemes: ILexemeResult[], previous: ILexemeResult) =>
+ previous && nestingReducer(0, lexemes) === 0;
+
+const ifExpression = (_: ILexemeResult[], previous: ILexemeResult) =>
+ previous && R.contains(
+ previous.lexeme.type,
+ [LexemeType.RelationalOperator]
+ );
+
+const ifLogicalOperator = (_: ILexemeResult[], previous: ILexemeResult) =>
+ previous && R.contains(
+ previous.lexeme.type,
+ [
+ LexemeType.BlockClose,
+ LexemeType.Expression,
+ LexemeType.UnaryOperator
+ ]
+ );
+
+const ifOperator = (_: ILexemeResult[], previous: ILexemeResult) =>
+ previous && R.contains(
+ previous.lexeme.type,
+ [LexemeType.Operand]
+ );
+
+const lexicon: ILexeme[] = [
+ {
+ ...and,
+ if: ifLogicalOperator,
+ terminal: false
+ },
+ {
+ ...or,
+ if: ifLogicalOperator,
+ terminal: false
+ },
+ {
+ ...blockClose,
+ if: (lexemes: ILexemeResult[], previous: ILexemeResult) =>
+ previous && R.contains(
+ previous.lexeme.type,
+ [
+ LexemeType.BlockClose,
+ LexemeType.BlockOpen,
+ LexemeType.Expression,
+ LexemeType.UnaryOperator
+ ]
+ ) && nestingReducer(0, lexemes) > 0,
+ terminal: isTerminal
+ },
+ {
+ ...blockOpen,
+ if: (_: ILexemeResult[], previous: ILexemeResult) =>
+ !previous || R.contains(
+ previous.lexeme.type,
+ [
+ LexemeType.BlockOpen,
+ LexemeType.LogicalOperator,
+ LexemeType.UnaryOperator
+ ]
+ ),
+ terminal: false
+ },
+ {
+ ...operand,
+ if: (_: ILexemeResult[], previous: ILexemeResult) =>
+ !previous || R.contains(
+ previous.lexeme.type,
+ [
+ LexemeType.BlockOpen,
+ LexemeType.LogicalOperator
+ ]
+ ),
+ terminal: false
+ },
+ ...[contains,
+ equal,
+ greaterOrEqual,
+ greaterThan,
+ lessOrEqual,
+ lessThan,
+ notEqual
+ ].map(op => ({
+ ...op,
+ if: ifOperator,
+ terminal: false
+ })),
+ ...[isBool,
+ isEven,
+ isNil,
+ isNum,
+ isObject,
+ isOdd,
+ isPrime,
+ isStr
+ ].map(op => ({
+ ...op,
+ if: ifOperator,
+ terminal: isTerminal
+ })),
+ {
+ ...not,
+ if: (_: ILexemeResult[], previous: ILexemeResult) =>
+ !previous || R.contains(
+ previous.lexeme.type,
+ [
+ LexemeType.LogicalOperator,
+ LexemeType.UnaryOperator
+ ]
+ ),
+ terminal: false
+ },
+ ...[
+ fieldExpression,
+ stringExpression,
+ valueExpression
+ ].map(exp => ({
+ ...exp,
+ if: ifExpression,
+ terminal: isTerminal
+ }))
+];
+
+export default lexicon;
\ No newline at end of file
diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts
index 7c8094004..fe2b2c856 100644
--- a/tests/cypress/tests/standalone/filtering_test.ts
+++ b/tests/cypress/tests/standalone/filtering_test.ts
@@ -4,77 +4,135 @@ import Key from 'cypress/Key';
import { AppMode } from 'demo/AppMode';
-describe(`filter`, () => {
- describe(`special characters`, () => {
- beforeEach(() => {
- cy.visit(`http://localhost:8080?mode=${AppMode.ColumnsInSpace}`);
- DashTable.toggleScroll(false);
- });
+describe(`filter special characters`, () => {
+ beforeEach(() => {
+ cy.visit(`http://localhost:8080?mode=${AppMode.ColumnsInSpace}`);
+ DashTable.toggleScroll(false);
+ });
+
+ it('can filter on special column id', () => {
+ DashTable.getFilterById('b+bb').click();
+ DOM.focused.type(`Wet${Key.Enter}`);
- it('can filter on special column id', () => {
- DashTable.getFilterById('c cc').click();
- DOM.focused.type(`gt num(90)${Key.Enter}`);
+ DashTable.getFilterById('c cc').click();
+ DOM.focused.type(`gt 90${Key.Enter}`);
- DashTable.getFilterById('d:dd').click();
- DOM.focused.type(`lt num(12500)${Key.Enter}`);
+ DashTable.getFilterById('d:dd').click();
+ DOM.focused.type(`lt 12500${Key.Enter}`);
- DashTable.getFilterById('e-ee').click();
- DOM.focused.type(`is prime${Key.Enter}`);
+ DashTable.getFilterById('e-ee').click();
+ DOM.focused.type(`is prime${Key.Enter}`);
- DashTable.getFilterById('f_ff').click();
- DOM.focused.type(`le num(106)${Key.Enter}`);
+ DashTable.getFilterById('f_ff').click();
+ DOM.focused.type(`le 106${Key.Enter}`);
- DashTable.getFilterById('g.gg').click();
- DOM.focused.type(`gt num(1000)${Key.Enter}`);
+ DashTable.getFilterById('g.gg').click();
+ DOM.focused.type(`gt 1000${Key.Enter}`);
+ DashTable.getFilterById('b+bb').click();
- DashTable.getFilterById('b+bb').click();
- DOM.focused.type(`eq "Wet"${Key.Enter}`);
+ DashTable.getCellById(0, 'rows').within(() => cy.get('.dash-cell-value').should('have.html', '101'));
+ DashTable.getCellById(1, 'rows').should('not.exist');
+ DashTable.getFilterById('b+bb').within(() => cy.get('input').should('have.value', 'Wet'));
+ DashTable.getFilterById('c cc').within(() => cy.get('input').should('have.value', 'gt 90'));
+ DashTable.getFilterById('d:dd').within(() => cy.get('input').should('have.value', 'lt 12500'));
+ DashTable.getFilterById('e-ee').within(() => cy.get('input').should('have.value', 'is prime'));
+ DashTable.getFilterById('f_ff').within(() => cy.get('input').should('have.value', 'le 106'));
+ DashTable.getFilterById('g.gg').within(() => cy.get('input').should('have.value', 'gt 1000'));
+ });
+});
+
+describe('filter', () => {
+ beforeEach(() => {
+ cy.visit(`http://localhost:8080?mode=${AppMode.Filtering}`);
+ DashTable.toggleScroll(false);
+ });
+
+ it('handles hovering onto other filtering cells', () => {
+ DashTable.getFilterById('ccc').click();
+ DOM.focused.type(`gt 100`);
+ DashTable.getFilterById('ddd').click();
+ DOM.focused.type('lt 20000');
+
+ DashTable.getCellById(0, 'eee').trigger('mouseover');
+
+ DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt 100'));
+ DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', 'lt 20000'));
+ });
- DashTable.getCellById(0, 'rows').within(() => cy.get('.dash-cell-value').should('have.html', '101'));
- DashTable.getCellById(1, 'rows').should('not.exist');
- });
+ it('handles invalid queries', () => {
+ let cell_0;
+ let cell_1;
+
+ DashTable.getCellById(0, 'ccc')
+ .within(() => cy.get('.dash-cell-value')
+ .then($el => cell_0 = $el[0].innerHTML));
+
+ DashTable.getCellById(1, 'ccc')
+ .within(() => cy.get('.dash-cell-value')
+ .then($el => cell_1 = $el[0].innerHTML));
+
+ DashTable.getFilterById('ccc').click();
+ DOM.focused.type(`gt`);
+ DashTable.getFilterById('ddd').click();
+ DOM.focused.type('20 a000');
+ DashTable.getFilterById('eee').click();
+ DOM.focused.type('is prime2');
+ DashTable.getFilterById('bbb').click();
+ 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('ccc').within(() => cy.get('input').should('have.value', 'gt'));
+ 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('ccc').should('have.class', 'invalid');
+ DashTable.getFilterById('ddd').should('have.class', 'invalid');
+ DashTable.getFilterById('eee').should('have.class', 'invalid');
});
- describe('reset', () => {
- beforeEach(() => {
- cy.visit(`http://localhost:8080?mode=${AppMode.Filtering}`);
- DashTable.toggleScroll(false);
- });
-
- it('updates results and filter fields', () => {
- let cell_0;
- let cell_1;
-
- DashTable.getCellById(0, 'ccc')
- .within(() => cy.get('.dash-cell-value')
- .then($el => cell_0 = $el[0].innerHTML));
-
- DashTable.getCellById(1, 'ccc')
- .within(() => cy.get('.dash-cell-value')
- .then($el => cell_1 = $el[0].innerHTML));
-
- DashTable.getFilterById('ccc').click();
- DOM.focused.type(`gt num(100)`);
- DashTable.getFilterById('ddd').click();
- DOM.focused.type('lt num(20000)');
- DashTable.getFilterById('eee').click();
- DOM.focused.type('is prime');
- DashTable.getFilterById('bbb').click();
- DOM.focused.type(`eq "Wet"`);
- DashTable.getFilterById('ccc').click();
-
- DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '101'));
- DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '109'));
-
- cy.get('.clear-filters').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('ccc').within(() => cy.get('input').should('have.value', ''));
- DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', ''));
- DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', ''));
- });
+ it('reset updates results and filter fields', () => {
+ let cell_0;
+ let cell_1;
+
+ DashTable.getCellById(0, 'ccc')
+ .within(() => cy.get('.dash-cell-value')
+ .then($el => cell_0 = $el[0].innerHTML));
+
+ DashTable.getCellById(1, 'ccc')
+ .within(() => cy.get('.dash-cell-value')
+ .then($el => cell_1 = $el[0].innerHTML));
+
+ DashTable.getFilterById('ccc').click();
+ DOM.focused.type(`gt 100`);
+ DashTable.getFilterById('ddd').click();
+ DOM.focused.type('lt 20000');
+ DashTable.getFilterById('eee').click();
+ DOM.focused.type('is prime');
+ DashTable.getFilterById('bbb').click();
+ DOM.focused.type(`Wet`);
+ DashTable.getFilterById('ccc').click();
+
+ DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '101'));
+ DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '109'));
+
+ DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', 'Wet'));
+ DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt 100'));
+ DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', 'lt 20000'));
+ DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', 'is prime'));
+
+ cy.get('.clear-filters').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('ccc').within(() => cy.get('input').should('have.value', ''));
+ DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', ''));
+ DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', ''));
});
});
\ No newline at end of file
diff --git a/tests/cypress/tests/unit/lexeme_test.ts b/tests/cypress/tests/unit/lexeme_test.ts
new file mode 100644
index 000000000..5c878e82c
--- /dev/null
+++ b/tests/cypress/tests/unit/lexeme_test.ts
@@ -0,0 +1,117 @@
+import {
+ fieldExpression,
+ stringExpression,
+ valueExpression
+} from 'dash-table/syntax-tree/lexeme/expression';
+import operand from 'dash-table/syntax-tree/lexeme/operand';
+import { ISyntaxTree } from 'core/syntax-tree/syntaxer';
+
+describe('expression', () => {
+ it('resolves field expression', () => {
+ expect(!!fieldExpression.resolve).to.equal(true);
+ expect(typeof fieldExpression.resolve).to.equal('function');
+
+ if (fieldExpression.resolve) {
+ expect(fieldExpression.resolve({ abc: 3 }, { value: '{abc}' } as ISyntaxTree)).to.equal(3);
+ expect(fieldExpression.resolve({ 'a bc': 3 }, { value: '{a bc}' } as ISyntaxTree)).to.equal(3);
+ expect(fieldExpression.resolve({ '{abc}': 3 }, { value: '{\\{abc\\}}' } as ISyntaxTree)).to.equal(3);
+ expect(fieldExpression.resolve({ '"abc"': 3 }, { value: '{"abc"}' } as ISyntaxTree)).to.equal(3);
+
+ expect(fieldExpression.resolve({ abc: 3 }, { value: '{def}' } as ISyntaxTree) === undefined).to.equal(true);
+ expect(fieldExpression.resolve({ abc: 3 }, { value: '{a bc}' } as ISyntaxTree) === undefined).to.equal(true);
+ expect(fieldExpression.resolve({ abc: 3 }, { value: '{"abc"}' } as ISyntaxTree) === undefined).to.equal(true);
+
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: '3' } as ISyntaxTree)).to.throw(Error);
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: 'abc' } as ISyntaxTree)).to.throw(Error);
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: '{abc' } as ISyntaxTree)).to.throw(Error);
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: 'abc}' } as ISyntaxTree)).to.throw(Error);
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: '}abc{' } as ISyntaxTree)).to.throw(Error);
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error);
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: '"' } as ISyntaxTree)).to.throw(Error);
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: '`' } as ISyntaxTree)).to.throw(Error);
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: `'` } as ISyntaxTree)).to.throw(Error);
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: '{' } as ISyntaxTree)).to.throw(Error);
+ expect(fieldExpression.resolve.bind(undefined, {}, { value: '}' } as ISyntaxTree)).to.throw(Error);
+ }
+ });
+
+ it('resolves string expressions', () => {
+ expect(!!stringExpression.resolve).to.equal(true);
+ expect(typeof stringExpression.resolve).to.equal('function');
+
+ if (stringExpression.resolve) {
+ expect(stringExpression.resolve(undefined, { value: '\'abc\'' } as ISyntaxTree)).to.equal('abc');
+ expect(stringExpression.resolve(undefined, { value: '"abc"' } as ISyntaxTree)).to.equal('abc');
+ expect(stringExpression.resolve(undefined, { value: '`abc`' } as ISyntaxTree)).to.equal('abc');
+ expect(stringExpression.resolve(undefined, { value: '"\\""' } as ISyntaxTree)).to.equal('"');
+ expect(stringExpression.resolve(undefined, { value: `'\\''` } as ISyntaxTree)).to.equal(`'`);
+ expect(stringExpression.resolve(undefined, { value: '`\\``' } as ISyntaxTree)).to.equal('`');
+ expect(stringExpression.resolve(undefined, { value: '\'\\\\\'' } as ISyntaxTree)).to.equal('\\');
+
+ expect(stringExpression.resolve.bind(undefined, {}, { value: '3' } as ISyntaxTree)).to.throw(Error);
+ expect(stringExpression.resolve.bind(undefined, {}, { value: 'abc' } as ISyntaxTree)).to.throw(Error);
+ expect(stringExpression.resolve.bind(undefined, {}, { value: '{abc' } as ISyntaxTree)).to.throw(Error);
+ expect(stringExpression.resolve.bind(undefined, {}, { value: 'abc}' } as ISyntaxTree)).to.throw(Error);
+ expect(stringExpression.resolve.bind(undefined, {}, { value: '}abc{' } as ISyntaxTree)).to.throw(Error);
+ expect(stringExpression.resolve.bind(undefined, {}, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error);
+ expect(stringExpression.resolve.bind(undefined, {}, { value: '"' } as ISyntaxTree)).to.throw(Error);
+ expect(stringExpression.resolve.bind(undefined, {}, { value: '`' } as ISyntaxTree)).to.throw(Error);
+ expect(stringExpression.resolve.bind(undefined, {}, { value: `'` } as ISyntaxTree)).to.throw(Error);
+ expect(stringExpression.resolve.bind(undefined, {}, { value: '{' } as ISyntaxTree)).to.throw(Error);
+ expect(stringExpression.resolve.bind(undefined, {}, { value: '}' } as ISyntaxTree)).to.throw(Error);
+ }
+ });
+
+ it('resolves value expressions', () => {
+ expect(!!valueExpression.resolve).to.equal(true);
+ expect(typeof valueExpression.resolve).to.equal('function');
+
+ if (valueExpression.resolve) {
+ expect(valueExpression.resolve(undefined, { value: 'abc' } as ISyntaxTree)).to.equal('abc');
+ expect(valueExpression.resolve(undefined, { value: 'abc ' } as ISyntaxTree)).to.equal('abc');
+ expect(valueExpression.resolve(undefined, { value: 'abc\\ \\ \\ ' } as ISyntaxTree)).to.equal('abc ');
+ expect(valueExpression.resolve(undefined, { value: '\\ \\ \\ abc' } as ISyntaxTree)).to.equal(' abc');
+ expect(valueExpression.resolve(undefined, { value: 'a\\ bc' } as ISyntaxTree)).to.equal('a bc');
+ expect(valueExpression.resolve(undefined, { value: '\\\\' } as ISyntaxTree)).to.equal('\\');
+ expect(valueExpression.resolve(undefined, { value: 'abc\\\\' } as ISyntaxTree)).to.equal('abc\\');
+ expect(valueExpression.resolve(undefined, { value: '123' } as ISyntaxTree)).to.equal(123);
+ expect(valueExpression.resolve(undefined, { value: '123.45' } as ISyntaxTree)).to.equal(123.45);
+ expect(valueExpression.resolve(undefined, { value: '1E6' } as ISyntaxTree)).to.equal(1000000);
+ expect(valueExpression.resolve(undefined, { value: '0x100' } as ISyntaxTree)).to.equal(256);
+ expect(valueExpression.resolve(undefined, { value: '\\{abc' } as ISyntaxTree)).to.equal('{abc');
+ expect(valueExpression.resolve(undefined, { value: 'abc\\}' } as ISyntaxTree)).to.equal('abc}');
+ expect(valueExpression.resolve(undefined, { value: '\\}abc\\{' } as ISyntaxTree)).to.equal('}abc{');
+ expect(valueExpression.resolve(undefined, { value: '\\{\\{abc\\}\\}' } as ISyntaxTree)).to.equal('{{abc}}');
+
+ expect(valueExpression.resolve.bind(undefined, {}, { value: '{abc' } as ISyntaxTree)).to.throw(Error);
+ expect(valueExpression.resolve.bind(undefined, {}, { value: 'abc}' } as ISyntaxTree)).to.throw(Error);
+ expect(valueExpression.resolve.bind(undefined, {}, { value: '}abc{' } as ISyntaxTree)).to.throw(Error);
+ expect(valueExpression.resolve.bind(undefined, {}, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error);
+ expect(valueExpression.resolve.bind(undefined, {}, { value: '"' } as ISyntaxTree)).to.throw(Error);
+ expect(valueExpression.resolve.bind(undefined, {}, { value: '`' } as ISyntaxTree)).to.throw(Error);
+ expect(valueExpression.resolve.bind(undefined, {}, { value: `'` } as ISyntaxTree)).to.throw(Error);
+ expect(valueExpression.resolve.bind(undefined, {}, { value: '{' } as ISyntaxTree)).to.throw(Error);
+ expect(valueExpression.resolve.bind(undefined, {}, { value: '}' } as ISyntaxTree)).to.throw(Error);
+ }
+ });
+});
+
+describe('operand', () => {
+ it('resolves values', () => {
+ expect(!!operand.resolve).to.equal(true);
+ expect(typeof operand.resolve).to.equal('function');
+
+ if (operand.resolve) {
+ expect(operand.resolve.bind(undefined, {}, { value: 'abc' } as ISyntaxTree)).to.throw(Error);
+ expect(operand.resolve.bind(undefined, {}, { value: '123' } as ISyntaxTree)).to.throw(Error);
+ expect(operand.resolve.bind(undefined, {}, { value: '{abc' } as ISyntaxTree)).to.throw(Error);
+ expect(operand.resolve.bind(undefined, {}, { value: 'abc}' } as ISyntaxTree)).to.throw(Error);
+ expect(operand.resolve.bind(undefined, {}, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error);
+
+ expect(operand.resolve({ abc: 3 }, { value: '{abc}' } as ISyntaxTree)).to.equal(3);
+ expect(operand.resolve({ ['a bc']: 3 }, { value: '{a bc}' } as ISyntaxTree)).to.equal(3);
+ expect(operand.resolve({ ['{abc}']: 3 }, { value: '{\\{abc\\}}' } as ISyntaxTree)).to.equal(3);
+ expect(operand.resolve({ ['"abc"']: 3 }, { value: '{"abc"}' } as ISyntaxTree)).to.equal(3);
+ }
+ });
+});
\ No newline at end of file
diff --git a/tests/cypress/tests/unit/multi_columns_syntactic_tree.ts b/tests/cypress/tests/unit/multi_columns_syntactic_tree.ts
new file mode 100644
index 000000000..1dd31c181
--- /dev/null
+++ b/tests/cypress/tests/unit/multi_columns_syntactic_tree.ts
@@ -0,0 +1,15 @@
+import { MultiColumnsSyntaxTree } from 'dash-table/syntax-tree';
+
+describe('Multi Columns Syntax Tree', () => {
+ it('can do single', () => {
+ const tree = new MultiColumnsSyntaxTree('{a} >= 3');
+
+ expect(tree.isValid).to.equal(true);
+ });
+
+ it('can "and"', () => {
+ const tree = new MultiColumnsSyntaxTree('{a} >= 3 && {b} is even');
+
+ expect(tree.isValid).to.equal(true);
+ });
+});
\ No newline at end of file
diff --git a/tests/cypress/tests/unit/syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts
similarity index 66%
rename from tests/cypress/tests/unit/syntactic_tree_test.ts
rename to tests/cypress/tests/unit/query_syntactic_tree_test.ts
index 59640a040..7fb011b0e 100644
--- a/tests/cypress/tests/unit/syntactic_tree_test.ts
+++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts
@@ -1,38 +1,65 @@
-import SyntaxTree from 'core/syntax-tree';
-
-describe('Syntax Tree', () => {
- const data0 = { a: '0', b: '0', c: 0, d: null, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot', '\'""\'': '0\'"dot' };
- const data1 = { a: '1', b: '0', c: 1, d: 0, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot', '_-6.:+** *@$': '1*dot', '\'""\'': '1\'"dot' };
- const data2 = { a: '2', b: '1', c: 2, d: '', 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot', '\'""\'': '2\'"dot' };
- const data3 = { a: '3', b: '1', c: 3, d: false, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot', '\'""\'': '3\'"dot' };
+import * as R from 'ramda';
+
+import { QuerySyntaxTree } from 'dash-table/syntax-tree';
+
+describe('Query Syntax Tree', () => {
+ const data0 = { a: '0', b: '0', c: 0, d: null, '\\{': 0, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot', '{a:dot}': '0*dot*', '\'""\'': '0\'"dot' };
+ const data1 = { a: '1', b: '0', c: 1, d: 0, '\\{': 1, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot', '_-6.:+** *@$': '1*dot', '{a:dot}': '1*dot*', '\'""\'': '1\'"dot' };
+ const data2 = { a: '2', b: '1', c: 2, d: '', '\\{': 2, 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot', '{a:dot}': '2*dot*', '\'""\'': '2\'"dot' };
+ const data3 = { a: '3', b: '1', c: 3, d: false, '\\{': 3, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot', '{a:dot}': '3*dot*', '\'""\'': '3\'"dot' };
+
+ describe('special whitespace characters are valid', () => {
+ const cases = [
+ { name: 'suports new line', query: '{a}\neq\n"0"' },
+ { name: 'suports carriage return', query: '{a}\req\r"0"' },
+ { name: 'suports new line ad carriage return combination', query: '{a}\r\neq\r\n"0"' },
+ { name: 'supports tab', query: '{a}\teq\t"0"' },
+ // some random non-standard whitespace character from https://en.wikipedia.org/wiki/Whitespace_character
+ { name: 'supports ogham space mark', query: '{a}\u1680eq\u1680"0"' },
+ { name: 'supports all', query: '{a}\r\n\t\u1680eq\r\n\t\u1680"0"' }
+ ];
+
+ R.forEach(c => {
+ it(c.name, () => {
+ const tree = new QuerySyntaxTree(c.query);
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate(data0)).to.equal(true);
+ });
+ }, cases);
+ });
describe('operands', () => {
it('does not support badly formed operands', () => {
- expect(new SyntaxTree(`'myField' eq num(0)`).isValid).to.equal(true);
- expect(new SyntaxTree(`"myField" eq num(0)`).isValid).to.equal(true);
- expect(new SyntaxTree('`myField` eq num(0)').isValid).to.equal(true);
- expect(new SyntaxTree(`'myField\\' eq num(0)`).isValid).to.equal(false);
- expect(new SyntaxTree(`"myField\\" eq num(0)`).isValid).to.equal(false);
- expect(new SyntaxTree('`myField\\` eq num(0)').isValid).to.equal(false);
- expect(new SyntaxTree(`\\'myField' eq num(0)`).isValid).to.equal(false);
- expect(new SyntaxTree(`\\"myField" eq num(0)`).isValid).to.equal(false);
- expect(new SyntaxTree('\\`myField` eq num(0)').isValid).to.equal(false);
+ expect(new QuerySyntaxTree(`{myField} eq 0`).isValid).to.equal(true);
+ expect(new QuerySyntaxTree(`{'myField'} eq 0`).isValid).to.equal(true);
+ expect(new QuerySyntaxTree(`{"myField"} eq 0`).isValid).to.equal(true);
+ expect(new QuerySyntaxTree('{`myField`} eq 0').isValid).to.equal(true);
+ expect(new QuerySyntaxTree('{\\{myField\\}} eq 0').isValid).to.equal(true);
+ expect(new QuerySyntaxTree('{{myField}} eq 0').isValid).to.equal(false);
+ expect(new QuerySyntaxTree(`{myField eq 0`).isValid).to.equal(false);
+ expect(new QuerySyntaxTree(`myField} eq 0`).isValid).to.equal(false);
+ expect(new QuerySyntaxTree(`myField eq 0`).isValid).to.equal(false);
+ expect(new QuerySyntaxTree('{\\\\{myField\\\\}} eq 0').isValid).to.equal(false);
});
it('does not support badly formed expression', () => {
- expect(new SyntaxTree(`myField eq 'value'`).isValid).to.equal(true);
- expect(new SyntaxTree(`myField eq "value"`).isValid).to.equal(true);
- expect(new SyntaxTree('myField eq `value`').isValid).to.equal(true);
- expect(new SyntaxTree(`myField eq 'value\\'`).isValid).to.equal(false);
- expect(new SyntaxTree(`myField eq "value\\"`).isValid).to.equal(false);
- expect(new SyntaxTree('myField eq `value\\`').isValid).to.equal(false);
- expect(new SyntaxTree(`myField eq \\'value'`).isValid).to.equal(false);
- expect(new SyntaxTree(`myField eq \\"value"`).isValid).to.equal(false);
- expect(new SyntaxTree('myField eq \\`value`').isValid).to.equal(false);
+ expect(new QuerySyntaxTree(`{myField} eq 'value'`).isValid).to.equal(true);
+ expect(new QuerySyntaxTree(`{myField} eq "value"`).isValid).to.equal(true);
+ expect(new QuerySyntaxTree('{myField} eq `value`').isValid).to.equal(true);
+ expect(new QuerySyntaxTree(`{myField} eq 'value\\\\'`).isValid).to.equal(true);
+ expect(new QuerySyntaxTree(`{myField} eq 'value\\''`).isValid).to.equal(true);
+ expect(new QuerySyntaxTree(`{myField} eq "value\\\\"`).isValid).to.equal(true);
+ expect(new QuerySyntaxTree('{myField} eq `value\\\\`').isValid).to.equal(true);
+ expect(new QuerySyntaxTree(`{myField} eq \\\\'value'`).isValid).to.equal(false);
+ expect(new QuerySyntaxTree(`{myField} eq \\\\"value"`).isValid).to.equal(false);
+ expect(new QuerySyntaxTree(`{myField} eq 'value\\'`).isValid).to.equal(false);
+ expect(new QuerySyntaxTree(`{myField} eq "value\\"`).isValid).to.equal(false);
+ expect(new QuerySyntaxTree('{myField} eq `value\\`').isValid).to.equal(false);
});
it('support arbitrary quoted column name', () => {
- const tree = new SyntaxTree(`'_-6.:+** *@$' eq "1*dot" || '_-6.:+** *@$' eq "2*dot"`);
+ const tree = new QuerySyntaxTree(`{_-6.:+** *@$} eq "1*dot" || {_-6.:+** *@$} eq "2*dot"`);
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -42,7 +69,7 @@ describe('Syntax Tree', () => {
});
it('support column name with "."', () => {
- const tree = new SyntaxTree('a.dot eq "1.dot" || a.dot eq "2.dot"');
+ const tree = new QuerySyntaxTree('{a.dot} eq "1.dot" || {a.dot} eq "2.dot"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -52,7 +79,7 @@ describe('Syntax Tree', () => {
});
it('support column name with "-"', () => {
- const tree = new SyntaxTree('a-dot eq "1-dot" || a-dot eq "2-dot"');
+ const tree = new QuerySyntaxTree('{a-dot} eq "1-dot" || {a-dot} eq "2-dot"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -62,7 +89,7 @@ describe('Syntax Tree', () => {
});
it('support column name with "_"', () => {
- const tree = new SyntaxTree('a_dot eq "1_dot" || a_dot eq "2_dot"');
+ const tree = new QuerySyntaxTree('{a_dot} eq "1_dot" || {a_dot} eq "2_dot"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -72,7 +99,7 @@ describe('Syntax Tree', () => {
});
it('support column name with "+"', () => {
- const tree = new SyntaxTree('a+dot eq "1+dot" || a+dot eq "2+dot"');
+ const tree = new QuerySyntaxTree('{a+dot} eq "1+dot" || {a+dot} eq "2+dot"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -82,7 +109,17 @@ describe('Syntax Tree', () => {
});
it('support column name with ":"', () => {
- const tree = new SyntaxTree('a:dot eq "1:dot" || a:dot eq "2:dot"');
+ const tree = new QuerySyntaxTree('{a:dot} eq "1:dot" || {a:dot} eq "2:dot"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate(data0)).to.equal(false);
+ expect(tree.evaluate(data1)).to.equal(true);
+ expect(tree.evaluate(data2)).to.equal(true);
+ expect(tree.evaluate(data3)).to.equal(false);
+ });
+
+ it('support column name with " " (space)', () => {
+ const tree = new QuerySyntaxTree('{a dot} eq "1 dot" || {a dot} eq "2 dot"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -91,8 +128,8 @@ describe('Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(false);
});
- it('support double quoted column name with " " (space)', () => {
- const tree = new SyntaxTree('"a dot" eq "1 dot" || "a dot" eq "2 dot"');
+ it('support column name with "{}"', () => {
+ const tree = new QuerySyntaxTree('{\\{a:dot\\}} eq "1*dot*" || {\\{a:dot\\}} eq "2*dot*"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -101,8 +138,8 @@ describe('Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(false);
});
- it('support single quoted column name with " " (space)', () => {
- const tree = new SyntaxTree('\'a dot\' eq "1 dot" || \'a dot\' eq "2 dot"');
+ it('support column name with "\\"', () => {
+ const tree = new QuerySyntaxTree('{\\\\\\{} eq 1 || {\\\\\\{} eq 2');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -112,7 +149,7 @@ describe('Syntax Tree', () => {
});
it('support nesting in quotes', () => {
- const tree = new SyntaxTree(`\`'""'\` eq \`1'"dot\` || \`'""'\` eq \`2'"dot\``);
+ const tree = new QuerySyntaxTree(`{'""'} eq '1\\'"dot' || {'""'} eq '2\\'"dot'`);
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -124,7 +161,7 @@ describe('Syntax Tree', () => {
describe('&& and ||', () => {
it('can || two conditions', () => {
- const tree = new SyntaxTree('a eq "1" || a eq "2"');
+ const tree = new QuerySyntaxTree('{a} eq "1" || {a} eq "2"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -134,7 +171,7 @@ describe('Syntax Tree', () => {
});
it('can "or" two conditions', () => {
- const tree = new SyntaxTree('a eq "1" or a eq "2"');
+ const tree = new QuerySyntaxTree('{a} eq "1" or {a} eq "2"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -144,7 +181,7 @@ describe('Syntax Tree', () => {
});
it('can && two conditions', () => {
- const tree = new SyntaxTree('a eq "1" && b eq "0"');
+ const tree = new QuerySyntaxTree('{a} eq "1" && {b} eq "0"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -154,7 +191,7 @@ describe('Syntax Tree', () => {
});
it('can "and" two conditions', () => {
- const tree = new SyntaxTree('a eq "1" and b eq "0"');
+ const tree = new QuerySyntaxTree('{a} eq "1" and {b} eq "0"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -164,7 +201,7 @@ describe('Syntax Tree', () => {
});
it('gives priority to && over ||', () => {
- const tree = new SyntaxTree('a eq "1" && a eq "0" || b eq "1"');
+ const tree = new QuerySyntaxTree('{a} eq "1" && {a} eq "0" || {b} eq "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -174,7 +211,7 @@ describe('Syntax Tree', () => {
});
it('gives priority to "and" over "or"', () => {
- const tree = new SyntaxTree('a eq "1" and a eq "0" or b eq "1"');
+ const tree = new QuerySyntaxTree('{a} eq "1" and {a} eq "0" or {b} eq "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -186,7 +223,7 @@ describe('Syntax Tree', () => {
describe('data types', () => {
it('can compare numbers', () => {
- const tree = new SyntaxTree('c eq num(1)');
+ const tree = new QuerySyntaxTree('{c} eq 1');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -196,7 +233,7 @@ describe('Syntax Tree', () => {
});
it('can compare floats', () => {
- const tree = new SyntaxTree('field ge num(1.5)');
+ const tree = new QuerySyntaxTree('{field} ge 1.5');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate({ field: -1.501 })).to.equal(false);
@@ -208,7 +245,7 @@ describe('Syntax Tree', () => {
});
it('can compare string to number and return false', () => {
- const tree = new SyntaxTree('a eq num(1)');
+ const tree = new QuerySyntaxTree('{a} eq 1');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -218,7 +255,7 @@ describe('Syntax Tree', () => {
});
it('can compare strings', () => {
- const tree = new SyntaxTree('a eq str(1)');
+ const tree = new QuerySyntaxTree('{a} eq "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -228,7 +265,7 @@ describe('Syntax Tree', () => {
});
it('can compare string to number and return false', () => {
- const tree = new SyntaxTree('c eq str(1)');
+ const tree = new QuerySyntaxTree('{c} eq "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -240,7 +277,7 @@ describe('Syntax Tree', () => {
describe('block', () => {
it('has priority over && and ||', () => {
- const tree = new SyntaxTree('a eq "1" && (a eq "0" || b eq "1")');
+ const tree = new QuerySyntaxTree('{a} eq "1" && ({a} eq "0" || {b} eq "1")');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -250,7 +287,7 @@ describe('Syntax Tree', () => {
});
it('gives priority over "and" and "or"', () => {
- const tree = new SyntaxTree('a eq "1" and (a eq "0" or b eq "1")');
+ const tree = new QuerySyntaxTree('{a} eq "1" and ({a} eq "0" or {b} eq "1")');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -260,7 +297,7 @@ describe('Syntax Tree', () => {
});
it('can be uselessly nested', () => {
- const tree = new SyntaxTree('((a eq "1" and (((a eq "0" or b eq "1")))))');
+ const tree = new QuerySyntaxTree('(({a} eq "1" and ((({a} eq "0" or {b} eq "1")))))');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -270,7 +307,7 @@ describe('Syntax Tree', () => {
});
it('can be nested', () => {
- const tree = new SyntaxTree('a eq "1" and (a eq "0" or (b eq "1" or b eq "0"))');
+ const tree = new QuerySyntaxTree('{a} eq "1" and ({a} eq "0" or ({b} eq "1" or {b} eq "0"))');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -278,11 +315,21 @@ describe('Syntax Tree', () => {
expect(tree.evaluate(data2)).to.equal(false);
expect(tree.evaluate(data3)).to.equal(false);
});
+
+ it('behave correctly with value expressions', () => {
+ const tree = new QuerySyntaxTree('{a} eq 1 and ({a} eq 0 or ({b} eq 1 or {b} eq 0))');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({ a: 0, b: 0 })).to.equal(false);
+ expect(tree.evaluate({ a: 0, b: 1 })).to.equal(false);
+ expect(tree.evaluate({ a: 1, b: 0 })).to.equal(true);
+ expect(tree.evaluate({ a: 1, b: 1 })).to.equal(true);
+ });
});
describe('unary operators', () => {
it('can check nil', () => {
- const tree = new SyntaxTree('d is nil');
+ const tree = new QuerySyntaxTree('{d} is nil');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(true);
@@ -292,7 +339,7 @@ describe('Syntax Tree', () => {
});
it('can invert check nil', () => {
- const tree = new SyntaxTree('!(d is nil)');
+ const tree = new QuerySyntaxTree('!({d} is nil)');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -302,7 +349,7 @@ describe('Syntax Tree', () => {
});
it('can check odd', () => {
- const tree = new SyntaxTree('c is odd');
+ const tree = new QuerySyntaxTree('{c} is odd');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -312,7 +359,7 @@ describe('Syntax Tree', () => {
});
it('can check odd on string and return false', () => {
- const tree = new SyntaxTree('a is odd');
+ const tree = new QuerySyntaxTree('{a} is odd');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -322,7 +369,7 @@ describe('Syntax Tree', () => {
});
it('can check even', () => {
- const tree = new SyntaxTree('c is even');
+ const tree = new QuerySyntaxTree('{c} is even');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(true);
@@ -332,7 +379,7 @@ describe('Syntax Tree', () => {
});
it('can check even on string and return false', () => {
- const tree = new SyntaxTree('a is even');
+ const tree = new QuerySyntaxTree('{a} is even');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -342,7 +389,7 @@ describe('Syntax Tree', () => {
});
it('can check if string', () => {
- const tree = new SyntaxTree('d is str');
+ const tree = new QuerySyntaxTree('{d} is str');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -352,7 +399,7 @@ describe('Syntax Tree', () => {
});
it('can check if number', () => {
- const tree = new SyntaxTree('d is num');
+ const tree = new QuerySyntaxTree('{d} is num');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -362,7 +409,7 @@ describe('Syntax Tree', () => {
});
it('can check if bool', () => {
- const tree = new SyntaxTree('d is bool');
+ const tree = new QuerySyntaxTree('{d} is bool');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -372,7 +419,7 @@ describe('Syntax Tree', () => {
});
it('can check if object', () => {
- const tree = new SyntaxTree('d is object');
+ const tree = new QuerySyntaxTree('{d} is object');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -384,7 +431,7 @@ describe('Syntax Tree', () => {
describe('unary not', () => {
it('can invert block', () => {
- const tree = new SyntaxTree('!(a eq "1")');
+ const tree = new QuerySyntaxTree('!({a} eq "1")');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(true);
@@ -394,7 +441,7 @@ describe('Syntax Tree', () => {
});
it('can invert block multiple times', () => {
- const tree = new SyntaxTree('!!(a eq "1")');
+ const tree = new QuerySyntaxTree('!!({a} eq "1")');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -406,7 +453,7 @@ describe('Syntax Tree', () => {
describe('logical binary operators', () => {
it('can do equality (eq) test', () => {
- const tree = new SyntaxTree('a eq "1"');
+ const tree = new QuerySyntaxTree('{a} eq "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -416,7 +463,7 @@ describe('Syntax Tree', () => {
});
it('can do equality (=) test', () => {
- const tree = new SyntaxTree('a = "1"');
+ const tree = new QuerySyntaxTree('{a} = "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -426,7 +473,7 @@ describe('Syntax Tree', () => {
});
it('can do difference (ne) test', () => {
- const tree = new SyntaxTree('a ne "1"');
+ const tree = new QuerySyntaxTree('{a} ne "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(true);
@@ -436,7 +483,7 @@ describe('Syntax Tree', () => {
});
it('can do difference (!=) test', () => {
- const tree = new SyntaxTree('a != "1"');
+ const tree = new QuerySyntaxTree('{a} != "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(true);
@@ -446,7 +493,7 @@ describe('Syntax Tree', () => {
});
it('can do greater than (gt) test', () => {
- const tree = new SyntaxTree('a gt "1"');
+ const tree = new QuerySyntaxTree('{a} gt "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -456,7 +503,7 @@ describe('Syntax Tree', () => {
});
it('can do greater than (>) test', () => {
- const tree = new SyntaxTree('a > "1"');
+ const tree = new QuerySyntaxTree('{a} > "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -466,7 +513,7 @@ describe('Syntax Tree', () => {
});
it('can do less than (lt) test', () => {
- const tree = new SyntaxTree('a lt "1"');
+ const tree = new QuerySyntaxTree('{a} lt "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(true);
@@ -476,7 +523,7 @@ describe('Syntax Tree', () => {
});
it('can do less than (<) test', () => {
- const tree = new SyntaxTree('a < "1"');
+ const tree = new QuerySyntaxTree('{a} < "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(true);
@@ -486,7 +533,7 @@ describe('Syntax Tree', () => {
});
it('can do greater or equal to (ge) test', () => {
- const tree = new SyntaxTree('a ge "1"');
+ const tree = new QuerySyntaxTree('{a} ge "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -496,7 +543,7 @@ describe('Syntax Tree', () => {
});
it('can do greater or equal to (>=) test', () => {
- const tree = new SyntaxTree('a >= "1"');
+ const tree = new QuerySyntaxTree('{a} >= "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(false);
@@ -506,7 +553,7 @@ describe('Syntax Tree', () => {
});
it('can do less or equal to (le) test', () => {
- const tree = new SyntaxTree('a le "1"');
+ const tree = new QuerySyntaxTree('{a} le "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(true);
@@ -516,7 +563,7 @@ describe('Syntax Tree', () => {
});
it('can do less or equal to (<=) test', () => {
- const tree = new SyntaxTree('a <= "1"');
+ const tree = new QuerySyntaxTree('{a} <= "1"');
expect(tree.isValid).to.equal(true);
expect(tree.evaluate(data0)).to.equal(true);
@@ -524,5 +571,13 @@ describe('Syntax Tree', () => {
expect(tree.evaluate(data2)).to.equal(false);
expect(tree.evaluate(data3)).to.equal(false);
});
+
+ it('can do contains (contains) test', () => {
+ const tree = new QuerySyntaxTree('{a} contains v');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({ a: 'abc v' })).to.equal(true);
+ expect(tree.evaluate({ a: 'abc w' })).to.equal(false);
+ });
});
});
\ No newline at end of file
diff --git a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts
new file mode 100644
index 000000000..dacdcc140
--- /dev/null
+++ b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts
@@ -0,0 +1,58 @@
+import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree';
+
+describe('Single Column Syntax Tree', () => {
+ it('cannot have operand', () => {
+ const tree = new SingleColumnSyntaxTree('a', '{a} <= 1');
+
+ expect(tree.isValid).to.equal(false);
+ });
+
+ it('cannot have binary dangle', () => {
+ const tree = new SingleColumnSyntaxTree('a', '<=');
+
+ expect(tree.isValid).to.equal(false);
+ });
+
+ it('cannot be unary + expression', () => {
+ const tree = new SingleColumnSyntaxTree('a', 'is prime "a"');
+
+ expect(tree.isValid).to.equal(false);
+ });
+
+ it('can be empty', () => {
+ const tree = new SingleColumnSyntaxTree('a', '');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({ a: 0 })).to.equal(true);
+ });
+
+ it('can be binary + expression', () => {
+ const tree = new SingleColumnSyntaxTree('a', '<= 1');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({ a: 0 })).to.equal(true);
+ expect(tree.evaluate({ a: 2 })).to.equal(false);
+
+ expect(tree.toQueryString()).to.equal('{a} <= 1');
+ });
+
+ it('can be unary', () => {
+ const tree = new SingleColumnSyntaxTree('a', 'is prime');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({ a: 5 })).to.equal(true);
+ expect(tree.evaluate({ a: 6 })).to.equal(false);
+
+ expect(tree.toQueryString()).to.equal('{a} is prime');
+ });
+
+ it('can be expression', () => {
+ const tree = new SingleColumnSyntaxTree('a', '1');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({ a: 1 })).to.equal(true);
+ expect(tree.evaluate({ a: 2 })).to.equal(false);
+
+ expect(tree.toQueryString()).to.equal('{a} eq 1');
+ });
+});
\ No newline at end of file
diff --git a/tests/dash/app_dataframe_backend_paging.py b/tests/dash/app_dataframe_backend_paging.py
index f9ffe0973..93fb53bc2 100644
--- a/tests/dash/app_dataframe_backend_paging.py
+++ b/tests/dash/app_dataframe_backend_paging.py
@@ -141,7 +141,7 @@ def layout():
pagination_mode='be',
filtering='be',
- filtering_settings=''
+ filter=''
),
section_title('Backend Paging with Filtering and Multi-Column Sorting'),
@@ -158,7 +158,7 @@ def layout():
pagination_mode='be',
filtering='be',
- filtering_settings='',
+ filter='',
sorting='be',
sorting_type='multi',
@@ -188,7 +188,7 @@ def layout():
pagination_mode='be',
filtering='be',
- filtering_settings='',
+ filter='',
sorting='be',
sorting_type='multi',
@@ -269,10 +269,10 @@ def update_graph(pagination_settings, sorting_settings):
@app.callback(
Output(IDS["table-filtering"], "data"),
[Input(IDS["table-filtering"], "pagination_settings"),
- Input(IDS["table-filtering"], "filtering_settings")])
-def update_graph(pagination_settings, filtering_settings):
- print(filtering_settings)
- filtering_expressions = filtering_settings.split(' && ')
+ Input(IDS["table-filtering"], "filter")])
+def update_graph(pagination_settings, filter):
+ print(filter)
+ filtering_expressions = filter.split(' && ')
dff = df
for filter in filtering_expressions:
if ' eq ' in filter:
@@ -298,9 +298,9 @@ def update_graph(pagination_settings, filtering_settings):
Output(IDS["table-sorting-filtering"], "data"),
[Input(IDS["table-sorting-filtering"], "pagination_settings"),
Input(IDS["table-sorting-filtering"], "sorting_settings"),
- Input(IDS["table-sorting-filtering"], "filtering_settings")])
-def update_graph(pagination_settings, sorting_settings, filtering_settings):
- filtering_expressions = filtering_settings.split(' && ')
+ Input(IDS["table-sorting-filtering"], "filter")])
+def update_graph(pagination_settings, sorting_settings, filter):
+ filtering_expressions = filter.split(' && ')
dff = df
for filter in filtering_expressions:
if ' eq ' in filter:
@@ -336,9 +336,9 @@ def update_graph(pagination_settings, sorting_settings, filtering_settings):
Output(IDS["table-paging-with-graph"], "data"),
[Input(IDS["table-paging-with-graph"], "pagination_settings"),
Input(IDS["table-paging-with-graph"], "sorting_settings"),
- Input(IDS["table-paging-with-graph"], "filtering_settings")])
-def update_table(pagination_settings, sorting_settings, filtering_settings):
- filtering_expressions = filtering_settings.split(' && ')
+ Input(IDS["table-paging-with-graph"], "filter")])
+def update_table(pagination_settings, sorting_settings, filter):
+ filtering_expressions = filter.split(' && ')
dff = df
for filter in filtering_expressions:
if ' eq ' in filter:
diff --git a/tests/dash/app_dataframe_updating_graph_fe.py b/tests/dash/app_dataframe_updating_graph_fe.py
index d2188bd19..beb8de26c 100644
--- a/tests/dash/app_dataframe_updating_graph_fe.py
+++ b/tests/dash/app_dataframe_updating_graph_fe.py
@@ -53,7 +53,7 @@ def layout():
> A quick note on filtering. We have defined our own
> syntax for performing filtering operations. Here are some
> examples for this particular dataset:
- > - `lt num(50)` in the `lifeExp` column
+ > - `lt 50` in the `lifeExp` column
> - `eq "Canada"` in the `country` column
By default, these transformations are done clientside.
diff --git a/tests/dash/app_dropdown.py b/tests/dash/app_dropdown.py
deleted file mode 100644
index c047a1315..000000000
--- a/tests/dash/app_dropdown.py
+++ /dev/null
@@ -1,190 +0,0 @@
-from collections import OrderedDict
-from dash.dependencies import Input, Output
-import dash_core_components as dcc
-import dash_html_components as html
-import pandas as pd
-from textwrap import dedent
-
-import dash_table
-from index import app
-from .utils import section_title
-
-
-ID_PREFIX = "app_dropdown"
-IDS = {
- "dropdown": ID_PREFIX,
- "dropdown-by-cell": '{}-row-by-cell'.format(ID_PREFIX),
- "dropdown-by-cell-deprecated": '{}-deprecated-row-by-cell'.format(ID_PREFIX)
-}
-
-
-df = pd.DataFrame(OrderedDict([
- ('climate',
- ['Sunny', 'Snowy', 'Sunny', 'Rainy']),
- ('temperature',
- [13, 43, 50, 30]),
- ('city',
- ['NYC', 'Montreal', 'Miami', 'NYC'])
-]))
-
-df_per_row_dropdown = pd.DataFrame(OrderedDict([
- ('City',
- ['NYC', 'Montreal', 'Los Angeles']),
- ('Neighborhood',
- ['Brooklyn', 'Mile End', 'Venice']),
- ('Temperature (F)',
- [70, 60, 90]),
-]))
-
-
-def layout():
- return html.Div([
-
- dcc.Markdown(dedent('''
- The Dash table includes support for per-column and
- per-cell dropdowns. In future releases, this will
- be tightly integrated with a more formal typing system.
-
- For now, use the dropdown renderer as a way to limit the
- options available when editing the values with an editable table.
-
- ''')),
-
- section_title('Dash Table with Per-Column Dropdowns'),
-
- dash_table.DataTable(
- id=IDS['dropdown'],
- data=df.to_dict('rows'),
- columns=[
- {'id': 'climate', 'name': 'climate'},
- {'id': 'temperature', 'name': 'temperature'},
- {'id': 'city', 'name': 'city'},
- ],
-
- editable=True,
- column_static_dropdown=[
- {
- 'id': 'climate',
- 'dropdown': [
- {'label': i, 'value': i}
- for i in df['climate'].unique()
- ]
- },
- {
- 'id': 'city',
- 'dropdown': [
- {'label': i, 'value': i}
- for i in df['city'].unique()
- ]
- },
- ]
- ),
-
- section_title('Dash Table with Per-Cell Dropdowns via Filtering UI'),
-
- dash_table.DataTable(
- id=IDS['dropdown-by-cell'],
- data=df_per_row_dropdown.to_dict('rows'),
- columns=[
- {'id': c, 'name': c}
- for c in df_per_row_dropdown.columns
- ],
-
- editable=True,
- column_conditional_dropdowns=[
- {
- 'id': 'Neighborhood',
- 'dropdowns': [
-
- {
- 'condition': 'City eq "NYC"',
- 'dropdown': [
- {'label': i, 'value': i}
- for i in [
- 'Brooklyn',
- 'Queens',
- 'Staten Island'
- ]
- ]
- },
-
- {
- 'condition': 'City eq "Montreal"',
- 'dropdown': [
- {'label': i, 'value': i}
- for i in [
- 'Mile End',
- 'Plateau',
- 'Hochelaga'
- ]
- ]
- },
-
- {
- 'condition': 'City eq "Los Angeles"',
- 'dropdown': [
- {'label': i, 'value': i}
- for i in [
- 'Venice',
- 'Hollywood',
- 'Los Feliz'
- ]
- ]
- }
-
- ]
- }
- ]
- ),
-
- section_title('Dash Table with Per-Cell Dropdowns'),
-
- html.Div('This example uses a deprecated API, `dropdown_properties`.'),
-
- dash_table.DataTable(
- id=IDS['dropdown-by-cell-deprecated'],
- data=df_per_row_dropdown.to_dict('rows'),
- columns=[
- {'id': c, 'name': c}
- for c in df_per_row_dropdown.columns
- ],
-
- editable=True,
- dropdown_properties=[
- {
- 'options': [
- {'label': i, 'value': i}
- for i in [
- 'Brooklyn',
- 'Queens',
- 'Staten Island'
- ]
- ]
- },
-
- {
- 'options': [
- {'label': i, 'value': i}
- for i in [
- 'Mile End',
- 'Plateau',
- 'Hochelaga'
- ]
- ]
- },
-
- {
- 'options': [
- {'label': i, 'value': i}
- for i in [
- 'Venice',
- 'Hollywood',
- 'Los Feliz'
- ]
- ]
- },
- ]
-
- ),
-
- ])
diff --git a/tests/dash/app_multi_header.py b/tests/dash/app_multi_header.py
deleted file mode 100644
index bfa719584..000000000
--- a/tests/dash/app_multi_header.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import dash_table
-import pandas as pd
-
-sanitized_name = __name__.replace(".", "-")
-
-def layout():
- df = pd.read_csv("datasets/gapminder.csv")
- return dash_table.DataTable(
- id=sanitized_name,
- columns=[
- {"name": ["Year", ""], "id": "year"},
- {"name": ["City", "Montreal"], "id": "montreal"},
- {"name": ["City", "Toronto"], "id": "toronto"},
- {"name": ["City", "Ottawa"], "id": "ottawa", "hidden": True},
- {"name": ["City", "Vancouver"], "id": "vancouver"},
- {"name": ["Climate", "Temperature"], "id": "temp"},
- {"name": ["Climate", "Humidity"], "id": "humidity"},
- ],
- data=[
- {
- "year": i,
- "montreal": i * 10,
- "toronto": i * 100,
- "ottawa": i * -1,
- "vancouver": i * -10,
- "temp": i * -100,
- "humidity": i * 0.1,
- }
- for i in range(100)
- ],
- merge_duplicate_headers=True,
- )
diff --git a/tests/dash/app_simple.py b/tests/dash/app_simple.py
deleted file mode 100644
index 134895151..000000000
--- a/tests/dash/app_simple.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import dash_table
-import pandas as pd
-
-sanitized_name = __name__.replace(".", "-")
-
-def layout():
- df = pd.read_csv("./datasets/gapminder.csv")
- return dash_table.DataTable(
- id=sanitized_name,
- columns=[{"name": i, "id": i} for i in df.columns],
- data=df.to_dict("rows"),
- )
diff --git a/tests/dash/app_sizing.py b/tests/dash/app_sizing.py
deleted file mode 100644
index 1657a2e10..000000000
--- a/tests/dash/app_sizing.py
+++ /dev/null
@@ -1,432 +0,0 @@
-from collections import OrderedDict
-from dash.dependencies import Input, Output
-import dash_core_components as dcc
-import dash_html_components as html
-import pandas as pd
-from textwrap import dedent
-
-import dash_table
-from index import app
-from .utils import section_title, html_table
-
-
-def layout():
- data = OrderedDict(
- [
- (
- "Date",
- [
- "July 12th, 2013 - July 25th, 2013",
- "July 12th, 2013 - August 25th, 2013",
- "July 12th, 2014 - August 25th, 2014",
- ],
- ),
- (
- "Election Polling Organization",
- ["The New York Times", "Pew Research", "The Washington Post"],
- ),
- ("Rep", [1, -20, 3.512]),
- ("Dem", [10, 20, 30]),
- ("Ind", [2, 10924, 3912]),
- (
- "Region",
- [
- "Northern New York State to the Southern Appalachian Mountains",
- "Canada",
- "Southern Vermont",
- ],
- ),
- ]
- )
-
- df = pd.DataFrame(data)
- df_long = pd.DataFrame(
- OrderedDict([(name, col_data * 10) for (name, col_data) in data.items()])
- )
-
- return [
- html.H1("Sizing Guide"),
- html.Div(
- style={
- "marginLeft": "auto",
- "marginRight": "auto",
- "width": "80%",
- "borderLeft": "thin hotpink solid",
- "borderRight": "thin hotpink solid",
- },
- children=[
- html.H1("Background - HTML Tables"),
- section_title("HTML Table - Default Styles"),
- html.Div("By default, HTML tables expand to their contents"),
- html_table(df, table_style={}, base_column_style={}),
- section_title("HTML Table - Padding"),
- html.Div(
- """
- Since the table content is packed so tightly,
- it's usually a good idea to place some left
- on the columns.
- """
- ),
- html_table(df, table_style={}, cell_style={"paddingLeft": 10}),
- section_title("HTML Table - Responsive Table"),
- html.Div(
- """
- With 100% width, the tables will expand to their
- container. When the table gets small, the text will break into
- multiple lines.
- """
- ),
- html_table(df, table_style={"width": "100%"}, base_column_style={}),
- section_title("HTML Table - All Column Widths defined by Percent"),
- html.Div(
- """
- The column widths can be definied by percents rather than pixels.
- """
- ),
- html_table(
- df,
- table_style={"width": "100%"},
- column_style={
- "Date": {"width": "30%"},
- "Election Polling Organization": {"width": "25%"},
- "Dem": {"width": "5%"},
- "Rep": {"width": "5%"},
- "Ind": {"width": "5%"},
- "Region": {"width": "30%"},
- },
- ),
- section_title("HTML Table - Single Column Width Defined by Percent"),
- html.Div(
- """
- The width of one column (Region=50%) can be definied by percent.
- """
- ),
- html_table(
- df,
- table_style={"width": "100%"},
- column_style={"Region": {"width": "50%"}},
- ),
- section_title("HTML Table - Columns with min-width"),
- html.Div(
- "Here, the min-width for the first column is 130px, or about the width of this line: "
- ),
- html.Div(
- style={"width": 130, "height": 10, "backgroundColor": "hotpink"}
- ),
- html_table(
- df,
- table_style={"width": "100%"},
- column_style={"Date": {"minWidth": "130"}},
- ),
- section_title("HTML Table - Underspecified Widths"),
- html.Div(
- """
- The widths can be under-specified. Here, we're only setting the width for the three
- columns in the middle, the rest of the columns are automatically sized to fit the rest of the container.
- The columns have a width of 50px, or the width of this line:
- """
- ),
- html.Div(
- style={"width": 50, "height": 10, "backgroundColor": "hotpink"}
- ),
- html_table(
- df,
- table_style={"width": "100%"},
- column_style={
- "Dem": {"width": 50},
- "Rep": {"width": 50},
- "Ind": {"width": 50},
- },
- ),
- section_title("HTML Table - Widths that are smaller than the content"),
- html.Div(
- """
- In this case, we're setting the width to 20px, which is smaller
- than the "10924" number in the "Ind" column.
- The table does not allow it.
- """
- ),
- html.Div(
- style={"width": 20, "height": 10, "backgroundColor": "hotpink"}
- ),
- html_table(
- df,
- table_style={"width": "100%"},
- column_style={
- "Dem": {"width": 20},
- "Rep": {"width": 20},
- "Ind": {"width": 20},
- },
- ),
- section_title("HTML Table - Content with Ellipses"),
- html.Div(
- """
- With `max-width`, the content can collapse into
- ellipses once the content doesn't fit.
-
- Here, `max-width` is set to 0. It could be any number, the only
- important thing is that it is supplied. The behaviour will be
- the same whether it is 0 or 50.
- """
- ),
- html_table(
- df,
- table_style={"width": "100%"},
- cell_style={
- "whiteSpace": "nowrap",
- "overflow": "hidden",
- "textOverflow": "ellipsis",
- "maxWidth": 0,
- },
- ),
- section_title("HTML Table - Vertical Scrolling"),
- html.Div(
- """
- By supplying a max-height of the Table container and supplying
- `overflow-y: scroll`, the table will become scrollable if the
- table's contents are larger than the container.
- """
- ),
- html.Div(
- style={"maxHeight": 300, "overflowY": "scroll"},
- children=html_table(df_long, table_style={"width": "100%"}),
- ),
- section_title("HTML Table - Vertical Scrolling with Max Height"),
- html.Div(
- """
- With `max-height`, if the table's contents are shorter than the
- `max-height`, then the container will be shorter.
- If you want a container with a constant height no matter the
- contents, then use `height`.
-
- Here, we're setting max-height to 300, or the height of this line:
- """
- ),
- html.Div(
- style={"width": 5, "height": 300, "backgroundColor": "hotpink"}
- ),
- html.Div(
- style={"maxHeight": 300, "overflowY": "scroll"},
- children=html_table(df, table_style={"width": "100%"}),
- ),
- section_title("HTML Table - Vertical Scrolling with Height"),
- html.Div("and here is `height` with the same content"),
- html.Div(
- style={"height": 300, "overflowY": "scroll"},
- children=html_table(df, table_style={"width": "100%"}),
- ),
- section_title("HTML Table - Horizontal Scrolling"),
- html.Div(
- """
- With HTML tables, we can set `min-width` to be 100%.
- If the content is small, then the columns will have some extra
- space.
- But if the content of any of the cells is really large, then the
- cells will expand beyond the container and a scrollbar will appear.
-
- In this way, `min-width` and `overflow-x: scroll` is an alternative
- to `text-overflow: ellipses`. With scroll, the content that can't
- fit in the container will get pushed out into a scrollable zone.
- With text-overflow: ellipses, the content will get truncated by
- ellipses. Both strategies work with or without line breaks on the
- white spaces (`white-space: normal` or `white-space: nowrap`).
-
- These next two examples have the same styles applied:
- - `min-width: 100%`
- - `white-space: nowrap` (to keep the content on a single line)
- - A parent with `overflow-x: scroll`
-
- """
- ),
- section_title("HTML Table - Two Columns, 100% Min-Width"),
- html.Div(
- html_table(
- pd.DataFrame({"Column 1": [1, 2], "Column 2": [3, 3]}),
- table_style={"minWidth": "100%"},
- cell_style={"whiteSpace": "nowrap"},
- ),
- style={"overflowX": "scroll"},
- ),
- section_title("HTML Table - Long Columns, 100% Min-Width"),
- html.Div(
- """
- Here is a table with several columns with long titles,
- 100% min-width, and `'white-space': 'nowrap'`
- (to keep the text on a single line)
- """
- ),
- html.Div(
- html_table(
- pd.DataFrame(
- {
- "This is Column {} Data".format(i): [1, 2]
- for i in range(10)
- }
- ),
- table_style={"minWidth": "100%", "overflowX": "scroll"},
- cell_style={"whiteSpace": "nowrap"},
- ),
- style={"overflowX": "scroll"},
- ),
- html.Hr(),
- html.H3("Dash Interactive Table"),
- html.Div("These same styles can be applied to the dash table"),
- section_title("Dash Table - Default Styles"),
- dash_table.DataTable(
- id="sizing-1",
- data=df.to_dict("rows"),
- columns=[{"name": i, "id": i} for i in df.columns],
- ),
- section_title("Dash Table - Padding"),
- # ...
- section_title("Dash Table - All Column Widths by Percent"),
- html.Div(
- """
- Here is a table with all columns having width equal to 16.67%,
- the Region column additionally wraps text. The table will try and respect
- the width of each column while allowing for the content to be displayed.
-
- Changing the browser's viewport width will help understand how the table
- allocates space.
- """
- ),
- dash_table.DataTable(
- id="sizing-2",
- data=df.to_dict("rows"),
- content_style="grow",
- columns=[
- {"name": i, "id": i} for i in df.columns
- ],
- css=[
- {"selector": ".dash-spreadsheet", "rule": "width: 100%"},
- {
- "selector": ".dash-cell[data-dash-column=Region]",
- "rule": "white-space: normal",
- },
- ],
- style_data_conditional=[{"width": "16.67%"}]
- ),
- section_title("Dash Table - Single Column Width by Percent"),
- html.Div(
- """
- Here is a table with all columns having default (auto) width excepts for the
- the Region column that has 50% width and wraps text. The table will try and respect
- the width of each column while allowing for the content to be displayed.
-
- Changing the browser's viewport width will help understand how the table
- allocates space.
- """
- ),
- dash_table.DataTable(
- id="sizing-3",
- data=df.to_dict("rows"),
- content_style="grow",
- columns=[
- {"name": i, "id": i }
- for i in df.columns
- ],
- css=[
- {"selector": ".dash-spreadsheet", "rule": "width: 100%"},
- {
- "selector": ".dash-cell[data-dash-column=Region]",
- "rule": "white-space: normal",
- },
- ],
- style_data_conditional=[{ "if": { "column_id": "Region" }, "width": "50%" }]
- ),
- section_title("Dash Table - Underspecified Widths"),
- html.Div(
- """
- The widths can be under-specified. Here, we're only setting the width for the three
- columns in the middle, the rest of the columns are automatically sized to fit the rest of the container.
- The columns have a width/minWidth/maxWidth of 100px.
- """
- ),
- dash_table.DataTable(
- id="sizing-4",
- data=df.to_dict("rows"),
- columns=[
- {
- "name": i,
- "id": i,
- }
- for i in df.columns
- ],
- style_data_conditional=[
- { "if": { "column_id": "Dem" }, "width": "100px", "min_width": "100px", "max_width": "100px" },
- { "if": { "column_id": "Rep" }, "width": "100px", "min_width": "100px", "max_width": "100px" },
- { "if": { "column_id": "Ind" }, "width": "100px", "min_width": "100px", "max_width": "100px" }
- ]
- ),
- section_title("Dash Table - Widths that are smaller than the content"),
- html.Div(
- """
- Width for all columns is set to 100px. Columns whose content is smaller than the defined size will respect it.
- Columns whose content is bigger than defined will grow to accomodate content. Region column wraps to show behavior
- in that case
- """
- ),
- dash_table.DataTable(
- id="sizing-5",
- data=df.to_dict("rows"),
- columns=[
- {"name": i, "id": i } for i in df.columns
- ],
- css=[
- {
- "selector": ".dash-cell[data-dash-column=Region]",
- "rule": "white-space: normal",
- }
- ],
- style_data_conditional=[{ "width": "100px" }]
- ),
- section_title(
- "Dash Table - Widths that are smaller than the content (forced)"
- ),
- html.Div(
- """
- Width/minWidth/maxWidth for all columns is set to 100px. Columns whose content is smaller than the defined size will respect it.
- Columns whose content is bigger than defined will respect it too. Region column wraps to show behavior
- in that case
- """
- ),
- dash_table.DataTable(
- id="sizing-6",
- data=df.to_dict("rows"),
- columns=[
- {
- "name": i,
- "id": i,
- }
- for i in df.columns
- ],
- css=[
- {
- "selector": ".dash-cell[data-dash-column=Region]",
- "rule": "white-space: normal",
- }
- ],
- content_style="fit",
- style_data_conditional=[
- { "width": "100px", "min_width": "100px", "max_width": "100px" }
- ]
- ),
- section_title("Dash Table - Content with Ellipses"),
- # ...
- section_title("Dash Table - Vertical Scrolling"),
- # ...
- section_title("Dash Table - Vertical Scrolling with Max Height"),
- # ...
- section_title("Dash Table - Vertical Scrolling with Height"),
- # ...
- section_title("Dash Table - Horizontal Scrolling"),
- # ...
- section_title("Dash Table - Two Columns, 100% Min-Width"),
- # ...
- section_title("Dash Table - Long Columns, 100% Min-Width"),
- # ...
- section_title("Dash Table - Alignment"),
- # ...
- ],
- ),
- ]
diff --git a/tests/dash/app_styling.py b/tests/dash/app_styling.py
deleted file mode 100644
index 9f9226817..000000000
--- a/tests/dash/app_styling.py
+++ /dev/null
@@ -1,454 +0,0 @@
-from collections import OrderedDict
-from dash.dependencies import Input, Output
-import dash_core_components as dcc
-import dash_html_components as html
-import pandas as pd
-from textwrap import dedent
-
-import dash_table
-from index import app
-from .utils import html_table, section_title
-
-
-def layout():
- data = OrderedDict(
- [
- ("Date", ["2015-01-01", "2015-10-24", "2016-05-10"] * 2),
- ("Region", ["Montreal", "Vermont", "New York City"] * 2),
- ("Temperature", [1, -20, 3.512] * 2),
- ("Humidity", [10, 20, 30] * 2),
- ("Pressure", [2, 10924, 3912] * 2),
- ]
- )
-
- df = pd.DataFrame(data)
-
- return html.Div(
- style={"marginLeft": "auto", "marginRight": "auto", "width": "80%"},
- children=[
- html.H1("[WIP] - Styling the Table"),
-
- section_title("HTML Table - Gridded"),
-
- html.Div("""
- By default, the Dash table has grey headers and borders
- around each cell. It resembles a spreadsheet with clearly defined
- headers
- """),
-
- html_table(
- df,
- cell_style={'border': 'thin lightgrey solid'},
- table_style={'width': '100%'},
- column_style={'width': '20%', 'paddingLeft': 20},
- header_style={'backgroundColor': 'rgb(235, 235, 235)'}
- ),
-
- html.Hr(),
-
- section_title("HTML Table - Column Alignment and Column Fonts"),
- dcc.Markdown(dedent(
- """
- When displaying numerical data, it's a good practice to use
- monospaced fonts, to right-align the data, and to provide the same
- number of decimals throughout the column.
-
- Note that it's not possible to modify the number of decimal places
- in css. `dash-table` will provide formatting options in the future,
- until then you'll have to modify your data before displaying it.
-
- For textual data, left-aligning the data is usually easier to read.
-
- In both cases, the column headers should have the same alignment
- as the cell content.
- """
- )),
- html_table(
- df,
- table_style={'width': '100%'},
- column_style={'width': '20%', 'paddingLeft': 20},
- cell_style={"paddingLeft": 5, "paddingRight": 5, 'border': 'thin lightgrey solid'},
- header_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- cell_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- header_style={'backgroundColor': 'rgb(235, 235, 235)'}
- ),
-
- html.Hr(),
-
- section_title('HTML Table - Styling the Table as a List'),
-
- dcc.Markdown(dedent('''
- The gridded view is a good default view for an editable table, like a spreadsheet.
- If your table isn't editable, then in many cases it can look cleaner without the
- horizontal or vertical grid lines.
- ''')),
-
- html_table(
- df,
- table_style={'width': '100%'},
- column_style={'width': '20%', 'paddingLeft': 20},
- cell_style={"paddingLeft": 5, "paddingRight": 5},
- header_style={'backgroundColor': 'rgb(235, 235, 235)', 'borderTop': 'thin lightgrey solid', 'borderBottom': 'thin lightgrey solid'},
- header_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- cell_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- ),
-
- html.Hr(),
-
- section_title('HTML Table - Row Padding'),
-
- dcc.Markdown(dedent('''
- By default, the gridded view is pretty tight. You can add some top and bottom row padding to
- the rows to give your data a little bit more room to breathe.
- ''')),
-
- html_table(
- df,
- table_style={'width': '100%'},
- column_style={'width': '20%', 'paddingLeft': 20},
- cell_style={"paddingLeft": 5, "paddingRight": 5},
- header_style={'backgroundColor': 'rgb(235, 235, 235)', 'borderTop': 'thin lightgrey solid', 'borderBottom': 'thin lightgrey solid'},
- header_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- cell_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- row_style={'paddingTop': 10, 'paddingBottom': 10}
- ),
-
- html.Hr(),
-
- section_title('HTML Table - List Style with Minimal Headers'),
-
- dcc.Markdown(dedent('''
- In some contexts, the grey background can look a little heavy.
- You can lighten this up by giving it a white background and
- a thicker bottom border.
- ''')),
-
- html_table(
- df,
- table_style={'width': '100%'},
- column_style={'width': '20%', 'paddingLeft': 20},
- cell_style={"paddingLeft": 5, "paddingRight": 5},
- header_style={'borderBottom': '2px lightgrey solid'},
- header_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- cell_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- row_style={'paddingTop': 10, 'paddingBottom': 10}
- ),
-
- html.Hr(),
-
- section_title('HTML Table - List Style with Understated Headers'),
-
- dcc.Markdown(dedent('''
- When the data is obvious, sometimes you can de-emphasize the headers
- as well, by giving them a lighter color than the cell text.
- ''')),
-
- html_table(
- df,
- table_style={'width': '100%'},
- column_style={'width': '20%', 'paddingLeft': 20},
- cell_style={"paddingLeft": 5, "paddingRight": 5},
- header_style={'color': 'rgb(100, 100, 100)'},
- header_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- cell_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- row_style={'paddingTop': 10, 'paddingBottom': 10}
- ),
-
- html.Hr(),
-
- section_title('HTML Table - Striped Rows'),
-
- dcc.Markdown(dedent('''
- When you're viewing datasets where you need to compare values within individual rows, it
- can sometimes be helpful to give the rows alternating background colors.
- We recommend using colors that are faded so as to not attract too much attention to the stripes.
- ''')),
-
- html_table(
- df,
- table_style={'width': '100%'},
- column_style={'width': '20%', 'paddingLeft': 20},
- cell_style={"paddingLeft": 5, "paddingRight": 5},
- header_style={'backgroundColor': 'rgb(235, 235, 235)', 'borderTop': 'thin lightgrey solid', 'borderBottom': 'thin lightgrey solid'},
- header_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- cell_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- row_style={'paddingTop': 10, 'paddingBottom': 10},
- odd_row_style={'backgroundColor': 'rgb(248, 248, 248)'}
- ),
-
- section_title('HTML Table - Dark Theme with Cells'),
-
- dcc.Markdown(dedent(
- """
- You have full control over all of the elements in the table.
- If you are viewing your table in an app with a dark background,
- you can provide inverted background and font colors.
- """
- )),
-
- html_table(
- df,
- table_style={
- 'backgroundColor': 'rgb(50, 50, 50)',
- 'color': 'white',
- 'width': '100%'
- },
- cell_style={'border': 'thin white solid'},
- header_style={'backgroundColor': 'rgb(30, 30, 30)'},
- row_style={'padding': 10},
- header_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- cell_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- ),
-
- section_title('HTML Table - Dark Theme with Rows'),
-
- html_table(
- df,
- table_style={
- 'backgroundColor': 'rgb(50, 50, 50)',
- 'color': 'white',
- 'width': '100%'
- },
- row_style={
- 'borderTop': 'thin white solid',
- 'borderBottom': 'thin white solid',
- 'padding': 10
- },
- header_style={'backgroundColor': 'rgb(30, 30, 30)'},
- header_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- cell_style_by_column={
- "Temperature": {"textAlign": "right", "fontFamily": "monospaced"},
- "Humidity": {"textAlign": "right", "fontFamily": "monospaced"},
- "Pressure": {"textAlign": "right", "fontFamily": "monospaced"},
- },
- ),
-
- section_title('HTML Table - Highlighting Certain Rows'),
-
- dcc.Markdown(dedent('''
- You can draw attention to certain rows by providing a unique
- background color, bold text, or colored text.
- ''')),
-
- html_table(
- df,
- cell_style={'border': 'thin lightgrey solid', 'color': 'rgb(60, 60, 60)'},
- table_style={'width': '100%'},
- column_style={'width': '20%', 'paddingLeft': 20},
- header_style={'backgroundColor': 'rgb(235, 235, 235)'},
- row_style_by_index={
- 4: {
- 'backgroundColor': 'yellow',
- }
- }
- ),
-
- section_title('HTML Table - Highlighting Certain Columns'),
-
- dcc.Markdown(dedent('''
- Similarly, certain columns can be highlighted.
- ''')),
-
- html_table(
- df,
- cell_style={'border': 'thin lightgrey solid', 'color': 'rgb(60, 60, 60)'},
- table_style={'width': '100%'},
- column_style={'width': '20%', 'paddingLeft': 20},
- header_style={'backgroundColor': 'rgb(235, 235, 235)'},
- cell_style_by_column={
- "Temperature": {
- "backgroundColor": "yellow"
- },
- }
- ),
-
- section_title('HTML Table - Highlighting Certain Cells'),
-
- dcc.Markdown(dedent('''
- You can also highlight certain cells. For example, you may want to
- highlight certain cells that exceed a threshold or that match
- a filter elsewhere in the app.
- ''')),
-
- html_table(
- df,
- cell_style={'border': 'thin lightgrey solid', 'color': 'rgb(60, 60, 60)'},
- table_style={'width': '100%'},
- column_style={'width': '20%', 'paddingLeft': 20},
- header_style={'backgroundColor': 'rgb(235, 235, 235)'},
- conditional_cell_style=lambda cell, column: (
- {'backgroundColor': 'yellow'}
- if (
- (column == 'Region' and cell == 'Montreal')
- or
- (cell == 20)
- ) else {}
- )
- ),
-
- html.Hr(),
-
- section_title('Dash Table - Styling the Table as a List'),
- # ...
-
- section_title('Dash Table - Row Padding'),
- dash_table.DataTable(
- id="styling-2",
- data=df.to_dict("rows"),
- columns=[
- {"name": i, "id": i} for i in df.columns
- ],
- style_data_conditional=[{ "padding_bottom": 5, "padding_top": 5}]
- ),
-
- section_title('Dash Table - List Style with Minimal Headers'),
- # ...
-
- section_title('Dash Table - List Style with Understated Headers'),
- # ...
-
- section_title('Dash Table - Striped Rows'),
- # ...
-
- section_title('Dash Table - Dark Theme with Cells'),
- dash_table.DataTable(
- id="styling-6",
- data=df.to_dict("rows"),
- columns=[
- {"name": i, "id": i} for i in df.columns
- ],
- content_style="grow",
- style_table={
- "width": "100%"
- },
- style_data_conditional=[{
- "background_color": "rgb(50, 50, 50)",
- "color": "white",
- "font_family": "arial"
- }, {
- "if": { "column_id": "Humidity" },
- "font_family": "monospace",
- "padding_left": 20,
- "text_align": "left"
- }, {
- "if": { "column_id": "Pressure" },
- "font_family": "monospace",
- "padding_left": 20,
- "text_align": "left"
- }, {
- "if": { "column_id": "Temperature" },
- "font_family": "monospace",
- "padding_left": 20,
- "text_align": "left"
- }]
- ),
-
- section_title('Dash Table - Dark Theme with Rows'),
- # ...
-
- section_title('Dash Table - Highlighting Certain Rows'),
- # ...
-
- section_title('Dash Table - Highlighting Certain Columns'),
- dash_table.DataTable(
- id="styling-9",
- data=df.to_dict("rows"),
- columns=[
- {"name": i, "id": i} for i in df.columns
- ],
- content_style="grow",
- style_table={
- "width": "100%"
- },
- style_data_conditional=[{
- "color": "rgb(60, 60, 60)",
- "padding_left": 20,
- "text-align": "left",
- "width": "20%"
- }, {
- "if": { "column_id": "Temperature" },
- "background_color": "yellow"
- }]
- ),
-
- section_title('Dash Table - Highlighting Certain Cells'),
- dash_table.DataTable(
- id="styling-10",
- data=df.to_dict("rows"),
- columns=[
- {"name": i, "id": i} for i in df.columns
- ],
- content_style="grow",
- style_table={
- "width": "100%"
- },
- style_data_conditional=[{
- "if": { "column_id": "Region", "filter": "Region eq str(Montreal)" },
- "background_color": "yellow"
- }, {
- "if": { "column_id": "Humidity", "filter": "Humidity eq num(20)" },
- "background_color": "yellow"
- }]
- )
- ],
- )
diff --git a/tests/dash/app_virtualization.py b/tests/dash/app_virtualization.py
deleted file mode 100644
index bc05b70f4..000000000
--- a/tests/dash/app_virtualization.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import dash_table
-import pandas as pd
-
-# Subset of https://www.irs.gov/pub/irs-soi/16zpallagi.csv
-df = pd.read_csv("./datasets/16zpallagi-25cols-100klines.csv")
-data = df.to_dict("rows")
-
-def layout():
- return dash_table.DataTable(
- columns=[{"name": i, "id": i} for i in df.columns],
- data=data,
- pagination_mode=False,
- virtualization=True,
- editable=True,
- n_fixed_rows=1,
- style_table={
- "height": 800,
- "max_height": 800,
- "width": 1300,
- "max_width": 1300
- },
- style_data={
- "width": 50,
- "max_width": 50,
- "min_width": 50
- }
- )
diff --git a/tests/dash/test_integration.py b/tests/dash/test_integration.py
index a19546cbf..631b4b7d6 100644
--- a/tests/dash/test_integration.py
+++ b/tests/dash/test_integration.py
@@ -21,7 +21,7 @@ def visit_and_snapshot(href):
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.ID, "waitfor"))
)
- time.sleep(2)
+ time.sleep(10)
self.snapshot(href)
self.driver.back()
diff --git a/tests/visual/percy-storybook/@Types/modules.d.ts b/tests/visual/percy-storybook/@Types/modules.d.ts
index 287edccf7..7f343088b 100644
--- a/tests/visual/percy-storybook/@Types/modules.d.ts
+++ b/tests/visual/percy-storybook/@Types/modules.d.ts
@@ -1,5 +1,11 @@
declare var module: any;
+declare module '*.csv' {
+ const value: any;
+
+ export default value;
+}
+
declare module '@storybook/react' {
export const storiesOf: any;
export const addDecorator: any;
diff --git a/tests/visual/percy-storybook/DashTable.percy.tsx b/tests/visual/percy-storybook/DashTable.percy.tsx
index c8e6fa184..a2ac626c1 100644
--- a/tests/visual/percy-storybook/DashTable.percy.tsx
+++ b/tests/visual/percy-storybook/DashTable.percy.tsx
@@ -1,6 +1,8 @@
+import parser from 'papaparse';
import * as R from 'ramda';
import React from 'react';
import { storiesOf } from '@storybook/react';
+
import random from 'core/math/random';
import DataTable from 'dash-table/dash/DataTable';
import fixtures from './fixtures';
@@ -11,6 +13,8 @@ const setProps = () => { };
const fixtureStories = storiesOf('DashTable/Fixtures', module);
fixtures.forEach(fixture => fixtureStories.add(fixture.name, () => ()));
+import dataset from './../../../datasets/gapminder.csv';
+
storiesOf('DashTable/Without Data', module)
.add('with 1 column', () => ());
storiesOf('DashTable/With Data', module)
+ .add('simple', () => {
+ const result = parser.parse(dataset, { delimiter: ',', header: true });
+
+ return ( ({ name: i, id: i }),
+ result.meta.fields)
+ }
+ />);
+ })
.add('with 3 columns and 3 rows, not actionable', () => ( { };
+
+const data = [
+ { climate: 'Sunny', temperature: 13, city: 'NYC' },
+ { climate: 'Snowy', temperature: 43, city: 'Montreal' },
+ { climate: 'Sunny', temperature: 50, city: 'Miami' },
+ { climate: 'Rainy', temperature: 30, city: 'NYC' }
+];
+
+const data2 = [
+ { City: 'NYC', Neighborhood: 'Brooklyn', 'Temperature (F)': 70 },
+ { City: 'Montreal', Neighborhood: 'Mile End', 'Temperature (F)': 60 },
+ { City: 'Los Angeles', Neighborhood: 'Venice', 'Temperature (F)': 90 }
+];
+
+const columns = R.map(
+ i => ({ name: i, id: i, presentation: 'dropdown' }),
+ ['climate', 'temperature', 'city']
+);
+
+const columns2 = R.map(
+ i => ({ name: i, id: i, presentation: 'dropdown' }),
+ ['City', 'Neighborhood', 'Temperature (F)']
+);
+
+storiesOf('DashTable/Dropdown', module)
+ .add('dropdown by column', () => ( ({ label: i, value: i }),
+ ['Sunny', 'Snowy', 'Rainy']
+ )
+ }, {
+ id: 'city',
+ dropdown: R.map(
+ i => ({ label: i, value: i }),
+ ['NYC', 'Montreal', 'Miami']
+ )
+ }]}
+ />))
+ .add('dropdown by filtering', () => ( ({ label: i, value: i }),
+ ['Brooklyn', 'Queens', 'Staten Island']
+ )
+ }, {
+ condition: '{City} eq "Montreal"',
+ dropdown: R.map(
+ i => ({ label: i, value: i }),
+ ['Mile End', 'Plateau', 'Hochelaga']
+ )
+ }, {
+ condition: '{City} eq "Los Angeles"',
+ dropdown: R.map(
+ i => ({ label: i, value: i }),
+ ['Venice', 'Hollywood', 'Los Feliz']
+ )
+ }]
+ }]}
+ />)).add('dropdown by cell (deprecated)', () => ( ({ label: i, value: i }),
+ ['Brooklyn', 'Queens', 'Staten Island']
+ )
+ }, {
+ options: R.map(
+ i => ({ label: i, value: i }),
+ ['Mile End', 'Plateau', 'Hochelaga']
+ )
+ }, {
+ options: R.map(
+ i => ({ label: i, value: i }),
+ ['Venice', 'Hollywood', 'Los Feliz']
+ )
+ }]
+ }}
+ />));
\ No newline at end of file
diff --git a/tests/visual/percy-storybook/Header.percy.tsx b/tests/visual/percy-storybook/Header.percy.tsx
new file mode 100644
index 000000000..e74780e43
--- /dev/null
+++ b/tests/visual/percy-storybook/Header.percy.tsx
@@ -0,0 +1,30 @@
+import * as R from 'ramda';
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import DataTable from 'dash-table/dash/DataTable';
+
+const setProps = () => { };
+
+storiesOf('DashTable/Headers', module)
+ .add('multi header', () => ( |