diff --git a/.eslintrc b/.eslintrc.js
similarity index 94%
rename from .eslintrc
rename to .eslintrc.js
index 684b718ae..0bdc4a10a 100644
--- a/.eslintrc
+++ b/.eslintrc.js
@@ -1,4 +1,6 @@
-{
+var path = require('path');
+
+module.exports = {
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier"
@@ -7,7 +9,7 @@
"@typescript-eslint"
],
"parserOptions": {
- "project": "./tsconfig.lint.json"
+ "project": path.join(__dirname, "tsconfig.lint.json"),
},
"rules": {
"arrow-parens": [
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 489cc5c58..835db9b85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
+## [Unreleased]
+### Added
+- [#545](https://github.com/plotly/dash-table/issues/545)
+ - Case insensitive filtering
+ - New props: `filter_options` - to control case of all filters, `columns.filter_options` - to control filter case for each column
+ - New operators: `i=`, `ieq`, `i>=`, `ige`, `i>`, `igt`, `i<=`, `ile`, `i<`, `ilt`, `i!=`, `ine`, `icontains` - for case-insensitive filtering, `s=`, `seq`, `s>=`, `sge`, `s>`, `sgt`, `s<=`, `sle`, `s<`, `slt`, `s!=`, `sne`, `scontains` - to force case-sensitive filtering on case-insensitive columns
+
## [4.11.3] - 2021-04-08
### Changed
- [#862](https://github.com/plotly/dash-table/pull/862) - update docstrings per https://github.com/plotly/dash/issues/1205
diff --git a/src/core/syntax-tree/lexer.ts b/src/core/syntax-tree/lexer.ts
index 56bcaa41a..5703edf75 100644
--- a/src/core/syntax-tree/lexer.ts
+++ b/src/core/syntax-tree/lexer.ts
@@ -10,6 +10,7 @@ export interface ILexerResult {
export interface ILexemeResult {
lexeme: ILexeme;
+ flags?: string;
value?: string;
}
@@ -37,8 +38,10 @@ export default function lexer(lexicon: Lexicon, query: string): ILexerResult {
return {lexemes: result, valid: false, error: query};
}
- const value = (query.match(next.regexp) || [])[next.regexpMatch || 0];
- result.push({lexeme: next, value});
+ const match = query.match(next.regexp) ?? [];
+ const value = match[next.regexpMatch || 0];
+ const flags = match[next.regexpFlags || -1];
+ result.push({lexeme: next, flags, value});
query = query.substring(value.length);
}
diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts
index b5e2c633e..4770efca8 100644
--- a/src/core/syntax-tree/lexicon.ts
+++ b/src/core/syntax-tree/lexicon.ts
@@ -19,6 +19,7 @@ export interface IUnboundedLexeme {
nesting?: number;
priority?: number;
regexp: RegExp;
+ regexpFlags?: number;
regexpMatch?: number;
syntaxer?: (lexs: any[], pivot: any, pivotIndex: number) => any;
transform?: (value: any) => any;
diff --git a/src/dash-table/components/Filter/Column.tsx b/src/dash-table/components/Filter/Column.tsx
index 1273303a7..d04ca8a55 100644
--- a/src/dash-table/components/Filter/Column.tsx
+++ b/src/dash-table/components/Filter/Column.tsx
@@ -2,17 +2,20 @@ import React, {CSSProperties, PureComponent} from 'react';
import IsolatedInput from 'core/components/IsolatedInput';
-import {ColumnId} from 'dash-table/components/Table/props';
+import {ColumnId, IFilterOptions} from 'dash-table/components/Table/props';
import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper';
+import FilterOptions from 'dash-table/components/Filter/FilterOptions';
type SetFilter = (ev: any) => void;
interface IColumnFilterProps {
className: string;
columnId: ColumnId;
+ filterOptions: IFilterOptions;
isValid: boolean;
setFilter: SetFilter;
style?: CSSProperties;
+ toggleFilterOptions: () => void;
value?: string;
}
@@ -41,7 +44,15 @@ export default class ColumnFilter extends PureComponent<
};
render() {
- const {className, columnId, isValid, style, value} = this.props;
+ const {
+ className,
+ columnId,
+ filterOptions,
+ isValid,
+ style,
+ toggleFilterOptions,
+ value
+ } = this.props;
return (
|
+
);
}
diff --git a/src/dash-table/components/Filter/FilterOptions.tsx b/src/dash-table/components/Filter/FilterOptions.tsx
new file mode 100644
index 000000000..34c231284
--- /dev/null
+++ b/src/dash-table/components/Filter/FilterOptions.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import {FilterCase, IFilterOptions} from 'dash-table/components/Table/props';
+
+interface IFilterCaseButtonProps {
+ filterOptions: IFilterOptions;
+ toggleFilterOptions: () => void;
+}
+
+export default ({
+ filterOptions,
+ toggleFilterOptions
+}: IFilterCaseButtonProps) => (
+
+);
diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx
index 0d834d6bc..90766ffc6 100644
--- a/src/dash-table/components/FilterFactory.tsx
+++ b/src/dash-table/components/FilterFactory.tsx
@@ -57,13 +57,27 @@ export default class FilterFactory {
updateColumnFilter(map, column, operator, value, setFilter);
};
+ private onToggleChange = (
+ column: IColumn,
+ map: Map,
+ operator: FilterLogicalOperator,
+ setFilter: SetFilter,
+ toggleFilterOptions: (column: IColumn) => IColumn,
+ value: any
+ ) => {
+ const newColumn = toggleFilterOptions(column);
+
+ updateColumnFilter(map, newColumn, operator, value, setFilter);
+ };
+
private filter = memoizerCache<[ColumnId, number]>()(
(
column: IColumn,
index: number,
map: Map,
operator: FilterLogicalOperator,
- setFilter: SetFilter
+ setFilter: SetFilter,
+ toggleFilterOptions: (column: IColumn) => IColumn
) => {
const ast = map.get(column.id.toString());
@@ -72,6 +86,7 @@ export default class FilterFactory {
key={`column-${index}`}
className={`dash-filter column-${index}`}
columnId={column.id}
+ filterOptions={column.filter_options}
isValid={!ast || ast.isValid}
setFilter={this.onChange.bind(
this,
@@ -80,6 +95,11 @@ export default class FilterFactory {
operator,
setFilter
)}
+ // Running into TypeScript binding issues with many parameters..
+ // bind with no more than 4 params each time.. sigh..
+ toggleFilterOptions={this.onToggleChange
+ .bind(this, column, map, operator, setFilter)
+ .bind(this, toggleFilterOptions, ast && ast.query)}
value={ast && ast.query}
/>
);
@@ -107,6 +127,7 @@ export default class FilterFactory {
style_cell_conditional,
style_filter,
style_filter_conditional,
+ toggleFilterOptions,
visibleColumns
} = this.props;
@@ -139,7 +160,8 @@ export default class FilterFactory {
index,
map,
filter_action.operator,
- setFilter
+ setFilter,
+ toggleFilterOptions
);
},
visibleColumns
diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less
index a303dbb0c..471e4f8ac 100644
--- a/src/dash-table/components/Table/Table.less
+++ b/src/dash-table/components/Table/Table.less
@@ -481,6 +481,23 @@
top: 0;
height: 100%;
width: 100%;
+
+ &.dash-filter--case {
+ position: relative;
+ left: auto;
+ top: auto;
+ width: auto;
+ height: 16px;
+ line-height: 0px;
+ padding: 1px;
+ }
+ &.dash-filter--case--sensitive {
+ border-color: hotpink;
+ border-radius: 3px;
+ border-style: solid;
+ border-width: 2px;
+ color: hotpink;
+ }
}
}
@@ -670,7 +687,7 @@
}
.dash-spreadsheet-inner {
- [class^='column-header--'] {
+ [class^='column-header--'], [class^='dash-filter--'] {
cursor: pointer;
float: left;
}
@@ -685,6 +702,7 @@
}
+ .dash-filter--case,
.column-header--clear,
.column-header--delete,
.column-header--edit,
@@ -695,10 +713,16 @@
}
th:hover {
- [class^='column-header--']:not(.disabled) {
- color: var(--accent);
- opacity: 1;
- }
+ [class^='column-header--'], [class^='dash-filter--'] {
+ &:not(.disabled) {
+ color: var(--accent);
+ opacity: 1;
+ }
+ }
+ }
+
+ .dash-filter--case {
+ font-size: 10px;
}
}
}
diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts
index ebe56ec7b..aa7249bd7 100644
--- a/src/dash-table/components/Table/props.ts
+++ b/src/dash-table/components/Table/props.ts
@@ -38,6 +38,11 @@ export enum ExportHeaders {
Display = 'display'
}
+export enum FilterCase {
+ Insensitive = 'insensitive',
+ Sensitive = 'sensitive'
+}
+
export enum SortMode {
Single = 'single',
Multi = 'multi'
@@ -49,6 +54,10 @@ export enum TableAction {
None = 'none'
}
+export interface IFilterOptions {
+ case?: FilterCase;
+}
+
export interface IDerivedData {
data: Data;
indices: Indices;
@@ -193,6 +202,7 @@ export interface IBaseColumn {
clearable?: boolean | boolean[] | 'first' | 'last';
deletable?: boolean | boolean[] | 'first' | 'last';
editable: boolean;
+ filter_options: IFilterOptions;
hideable?: boolean | boolean[] | 'first' | 'last';
renamable?: boolean | boolean[] | 'first' | 'last';
selectable?: boolean | boolean[] | 'first' | 'last';
@@ -327,6 +337,7 @@ export interface IProps {
data?: Data;
editable?: boolean;
fill_width?: boolean;
+ filter_options?: IFilterOptions;
filter_query?: string;
filter_action?: TableAction | IFilterAction;
hidden_columns?: string[];
@@ -381,6 +392,7 @@ interface IDefaultProps {
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
+ filter_options?: IFilterOptions;
filter_query: string;
filter_action: TableAction;
fixed_columns: Fixed;
@@ -492,6 +504,7 @@ export interface IFilterFactoryProps {
style_cell_conditional: Cells;
style_filter: Style;
style_filter_conditional: BasicFilters;
+ toggleFilterOptions: (column: IColumn) => IColumn;
visibleColumns: Columns;
}
diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js
index 2b7b5db25..53ca225e3 100644
--- a/src/dash-table/dash/DataTable.js
+++ b/src/dash-table/dash/DataTable.js
@@ -56,6 +56,7 @@ export const defaultProps = {
dropdown_data: [],
fill_width: true,
+ filter_options: {},
fixed_columns: {
headers: false,
data: 0
@@ -181,6 +182,19 @@ export const propTypes = {
*/
editable: PropTypes.bool,
+ /**
+ * There are two `filter_options` props in the table.
+ * This is the column-level filter_options prop and there is
+ * also the table-level `filter_options` prop.
+ * These props determine whether the applicable filter relational
+ * operators will default to `sensitive` or `insensitive` comparison.
+ * If the column-level `filter_options` prop is set it overrides
+ * the table-level `filter_options` prop for that column.
+ */
+ filter_options: PropTypes.shape({
+ case: PropTypes.oneOf(['sensitive', 'insensitive'])
+ }),
+
/**
* If true, the user can hide the column by clicking on the `hide`
* action button on the column. If there are multiple header rows, true
@@ -1142,6 +1156,18 @@ export const propTypes = {
})
]),
+ /**
+ * There are two `filter_options` props in the table.
+ * This is the table-level filter_options prop and there is
+ * also the column-level `filter_options` prop.
+ * These props determine whether the applicable filter relational
+ * operators will default to `sensitive` or `insensitive` comparison.
+ * If the column-level `filter_options` prop is set it overrides
+ * the table-level `filter_options` prop for that column.
+ */
+ filter_options: PropTypes.shape({
+ case: PropTypes.oneOf(['sensitive', 'insensitive'])
+ }),
/**
* The `sort_action` property enables data to be
* sorted on a per-column basis.
diff --git a/src/dash-table/dash/Sanitizer.ts b/src/dash-table/dash/Sanitizer.ts
index 5aaef28c3..c3d4e8f1a 100644
--- a/src/dash-table/dash/Sanitizer.ts
+++ b/src/dash-table/dash/Sanitizer.ts
@@ -16,7 +16,9 @@ import {
ExportHeaders,
IFilterAction,
FilterLogicalOperator,
- SelectedCells
+ SelectedCells,
+ FilterCase,
+ IFilterOptions
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
@@ -35,6 +37,10 @@ const DEFAULT_NULLY = '';
const DEFAULT_SPECIFIER = '';
const NULL_SELECTED_CELLS: SelectedCells = [];
+const DEFAULT_FILTER_OPTIONS = {
+ case: FilterCase.Sensitive
+};
+
const data2number = (data?: any) => +data || 0;
const getFixedColumns = (
@@ -63,11 +69,17 @@ const applyDefaultsToColumns = (
defaultLocale: INumberLocale,
defaultSort: SortAsNull,
columns: Columns,
- editable: boolean
+ editable: boolean,
+ filterOptions: IFilterOptions | undefined
) =>
R.map(column => {
const c = R.clone(column);
c.editable = resolveFlag(editable, column.editable);
+ c.filter_options = {
+ ...DEFAULT_FILTER_OPTIONS,
+ ...(filterOptions ?? {}),
+ ...(c.filter_options ?? {})
+ };
c.sort_as_null = c.sort_as_null || defaultSort;
if (c.type === ColumnType.Numeric && c.format) {
@@ -105,7 +117,8 @@ export default class Sanitizer {
locale_format,
props.sort_as_null,
props.columns,
- props.editable
+ props.editable,
+ props.filter_options
)
: [];
const data = props.data ?? [];
diff --git a/src/dash-table/derived/cell/resolveFlag.ts b/src/dash-table/derived/cell/resolveFlag.ts
index a60717ce3..a790f1c8b 100644
--- a/src/dash-table/derived/cell/resolveFlag.ts
+++ b/src/dash-table/derived/cell/resolveFlag.ts
@@ -1,2 +1,4 @@
-export default (tableFlag: boolean, columnFlag: boolean | undefined): boolean =>
- columnFlag === undefined ? tableFlag : columnFlag;
+import * as R from 'ramda';
+
+export default (tableFlag: T, columnFlag: T | null | undefined): T =>
+ R.isNil(columnFlag) ? tableFlag : columnFlag;
diff --git a/src/dash-table/derived/table/index.tsx b/src/dash-table/derived/table/index.tsx
index ebd0399ce..f344b5a26 100644
--- a/src/dash-table/derived/table/index.tsx
+++ b/src/dash-table/derived/table/index.tsx
@@ -9,6 +9,8 @@ import HeaderFactory from 'dash-table/components/HeaderFactory';
import {clearSelection} from 'dash-table/utils/actions';
import {
ControlledTableProps,
+ FilterCase,
+ IColumn,
SetProps,
SetState
} from 'dash-table/components/Table/props';
@@ -26,10 +28,18 @@ const handleSetFilter = (
setState({workFilter: {map, value: filter_query}, rawFilterQuery});
};
-function propsAndMapFn(propsFn: () => ControlledTableProps, setFilter: any) {
+function propsAndMapFn(
+ propsFn: () => ControlledTableProps,
+ setFilter: any,
+ toggleFilterOptions: (column: IColumn) => IColumn
+) {
const props = propsFn();
- return R.merge(props, {map: props.workFilter.map, setFilter});
+ return R.merge(props, {
+ map: props.workFilter.map,
+ setFilter,
+ toggleFilterOptions
+ });
}
export default (propsFn: () => ControlledTableProps) => {
@@ -37,6 +47,28 @@ export default (propsFn: () => ControlledTableProps) => {
handleSetFilter.bind(undefined, setProps, setState)
);
+ const toggleFilterOptions = memoizeOne(
+ (setProps: SetProps, columns: IColumn[]) => (column: IColumn) => {
+ const newColumns = [...columns];
+ const iColumn = columns.indexOf(column);
+
+ const newColumn = {...newColumns[iColumn]};
+ newColumn.filter_options = {
+ ...newColumn.filter_options,
+ case:
+ newColumn.filter_options.case === FilterCase.Insensitive
+ ? FilterCase.Sensitive
+ : FilterCase.Insensitive
+ };
+
+ newColumns.splice(iColumn, 1, newColumn);
+
+ setProps({columns: newColumns});
+
+ return newColumn;
+ }
+ );
+
const cellFactory = new CellFactory(propsFn);
const augmentedPropsFn = () => {
@@ -44,7 +76,8 @@ export default (propsFn: () => ControlledTableProps) => {
return propsAndMapFn(
propsFn,
- setFilter(props.setProps, props.setState)
+ setFilter(props.setProps, props.setState),
+ toggleFilterOptions(props.setProps, props.columns)
);
};
diff --git a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts
index a25c34d7e..d0f9c7abf 100644
--- a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts
+++ b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts
@@ -1,9 +1,16 @@
+import * as R from 'ramda';
+
import {RequiredPluck, OptionalPluck} from 'core/type';
import SyntaxTree from 'core/syntax-tree';
import {ILexemeResult, ILexerResult} from 'core/syntax-tree/lexer';
import {LexemeType, boundLexeme} from 'core/syntax-tree/lexicon';
-import {ColumnType, IColumn} from 'dash-table/components/Table/props';
+import {
+ ColumnType,
+ FilterCase,
+ IColumn,
+ IFilterOptions
+} from 'dash-table/components/Table/props';
import {fieldExpression} from './lexeme/expression';
import {
@@ -15,13 +22,58 @@ import {
import columnLexicon from './lexicon/column';
-function getImplicitLexeme(type: ColumnType = ColumnType.Any): ILexemeResult {
+const sensitiveRelationalOperators: string[] = [
+ RelationalOperator.Contains,
+ RelationalOperator.Equal,
+ RelationalOperator.GreaterOrEqual,
+ RelationalOperator.GreaterThan,
+ RelationalOperator.LessOrEqual,
+ RelationalOperator.LessThan,
+ RelationalOperator.NotEqual
+];
+
+function getFilterLexeme(
+ filterOptions: IFilterOptions | undefined,
+ lexeme: ILexemeResult
+): ILexemeResult {
+ const flags = R.isNil(filterOptions)
+ ? ''
+ : filterOptions.case === FilterCase.Insensitive
+ ? 'i'
+ : 's';
+
+ if (
+ lexeme.lexeme.type === LexemeType.RelationalOperator &&
+ lexeme.lexeme.subType &&
+ sensitiveRelationalOperators.indexOf(lexeme.lexeme.subType) !== -1 &&
+ lexeme.value &&
+ ['i', 's'].indexOf(lexeme.value[0]) === -1
+ ) {
+ return {
+ ...lexeme,
+ value: `${flags}${lexeme.value}`
+ };
+ }
+
+ return lexeme;
+}
+
+function getImplicitLexeme(
+ filterOptions: IFilterOptions | undefined,
+ type: ColumnType = ColumnType.Any
+): ILexemeResult {
+ const flags = R.isNil(filterOptions)
+ ? ''
+ : filterOptions.case === FilterCase.Insensitive
+ ? 'i'
+ : 's';
+
switch (type) {
case ColumnType.Any:
case ColumnType.Text:
return {
lexeme: boundLexeme(contains),
- value: RelationalOperator.Contains
+ value: `${flags}${RelationalOperator.Contains}`
};
case ColumnType.Datetime:
return {
@@ -31,7 +83,7 @@ function getImplicitLexeme(type: ColumnType = ColumnType.Any): ILexemeResult {
case ColumnType.Numeric:
return {
lexeme: boundLexeme(equal),
- value: RelationalOperator.Equal
+ value: `${flags}${RelationalOperator.Equal}`
};
}
}
@@ -58,7 +110,13 @@ function modifyLex(config: SingleColumnConfig, res: ILexerResult) {
return res;
}
- if (isBinary(res.lexemes) || isUnary(res.lexemes)) {
+ if (isBinary(res.lexemes)) {
+ res.lexemes = [
+ {lexeme: boundLexeme(fieldExpression), value: `{${config.id}}`},
+ getFilterLexeme(config.filter_options, res.lexemes[0]),
+ res.lexemes[1]
+ ];
+ } else if (isUnary(res.lexemes)) {
res.lexemes = [
{lexeme: boundLexeme(fieldExpression), value: `{${config.id}}`},
...res.lexemes
@@ -66,7 +124,7 @@ function modifyLex(config: SingleColumnConfig, res: ILexerResult) {
} else if (isExpression(res.lexemes)) {
res.lexemes = [
{lexeme: boundLexeme(fieldExpression), value: `{${config.id}}`},
- getImplicitLexeme(config.type),
+ getImplicitLexeme(config.filter_options, config.type),
...res.lexemes
];
}
@@ -75,6 +133,7 @@ function modifyLex(config: SingleColumnConfig, res: ILexerResult) {
}
export type SingleColumnConfig = RequiredPluck &
+ OptionalPluck &
OptionalPluck;
export default class SingleColumnSyntaxTree extends SyntaxTree {
diff --git a/src/dash-table/syntax-tree/lexeme/relational.ts b/src/dash-table/syntax-tree/lexeme/relational.ts
index 3701a3490..3e5765c40 100644
--- a/src/dash-table/syntax-tree/lexeme/relational.ts
+++ b/src/dash-table/syntax-tree/lexeme/relational.ts
@@ -7,7 +7,7 @@ import {ISyntaxTree} from 'core/syntax-tree/syntaxer';
import {normalizeDate} from 'dash-table/type/date';
import {IDateValidation} from 'dash-table/components/Table/props';
-function evaluator(target: any, tree: ISyntaxTree): [any, any] {
+function evaluator(target: any, tree: ISyntaxTree): [any, any, string] {
Logger.trace('evaluate -> relational', target, tree);
const t = tree as any;
@@ -16,7 +16,7 @@ function evaluator(target: any, tree: ISyntaxTree): [any, any] {
const expValue = t.right.lexeme.resolve(target, t.right);
Logger.trace(`opValue: ${opValue}, expValue: ${expValue}`);
- return [opValue, expValue];
+ return [opValue, expValue, tree.value];
}
function relationalSyntaxer([left, lexeme, right]: any[]) {
@@ -44,17 +44,41 @@ const LEXEME_BASE = {
type: LexemeType.RelationalOperator
};
+const containsEval = (lhs: any, rhs: any, relOp: string): boolean =>
+ relOp[0] == 'i'
+ ? lhs.toString().toUpperCase().indexOf(rhs.toString().toUpperCase()) !==
+ -1
+ : lhs.toString().indexOf(rhs.toString()) !== -1;
+
+const equalEval = (lhs: any, rhs: any, relOp: string): boolean =>
+ isNumeric(lhs) && isNumeric(rhs)
+ ? +lhs === +rhs
+ : relOp[0] == 'i'
+ ? lhs.toString().toUpperCase() === rhs.toString().toUpperCase()
+ : lhs === rhs;
+
+const fnEval = (
+ fn: (lhs: any, rhs: any) => boolean,
+ lhs: any,
+ rhs: any,
+ relOp: string
+): boolean =>
+ relOp[0] == 'i'
+ ? fn(lhs.toString().toUpperCase(), rhs.toString().toUpperCase())
+ : fn(lhs, rhs);
+
export const contains: IUnboundedLexeme = R.merge(
{
evaluate: relationalEvaluator(
- ([op, exp]) =>
- !R.isNil(exp) &&
- !R.isNil(op) &&
- (R.type(exp) === 'String' || R.type(op) === 'String') &&
- op.toString().indexOf(exp.toString()) !== -1
+ ([lhs, rhs, relOp]) =>
+ !R.isNil(rhs) &&
+ !R.isNil(lhs) &&
+ (R.type(rhs) === 'String' || R.type(lhs) === 'String') &&
+ containsEval(lhs, rhs, relOp)
),
subType: RelationalOperator.Contains,
- regexp: /^((contains)(?=\s|$))/i,
+ regexp: /^((i|s)?contains)(?=\s|$)/i,
+ regexpFlags: 2,
regexpMatch: 1
},
LEXEME_BASE
@@ -62,11 +86,12 @@ export const contains: IUnboundedLexeme = R.merge(
export const equal: IUnboundedLexeme = R.merge(
{
- evaluate: relationalEvaluator(([op, exp]) =>
- isNumeric(op) && isNumeric(exp) ? +op === +exp : op === exp
+ evaluate: relationalEvaluator(([lhs, rhs, relOp]) =>
+ equalEval(lhs, rhs, relOp)
),
subType: RelationalOperator.Equal,
- regexp: /^(=|(eq)(?=\s|$))/i,
+ regexp: /^((i|s)?(=|(eq)(?=\s|$)))/i,
+ regexpFlags: 2,
regexpMatch: 1
},
LEXEME_BASE
@@ -74,9 +99,12 @@ export const equal: IUnboundedLexeme = R.merge(
export const greaterOrEqual: IUnboundedLexeme = R.merge(
{
- evaluate: relationalEvaluator(([op, exp]) => op >= exp),
+ evaluate: relationalEvaluator(([lhs, rhs, relOp]) =>
+ fnEval((l, r) => l >= r, lhs, rhs, relOp)
+ ),
subType: RelationalOperator.GreaterOrEqual,
- regexp: /^(>=|(ge)(?=\s|$))/i,
+ regexp: /^((i|s)?(>=|(ge)(?=\s|$)))/i,
+ regexpFlags: 2,
regexpMatch: 1
},
LEXEME_BASE
@@ -84,9 +112,12 @@ export const greaterOrEqual: IUnboundedLexeme = R.merge(
export const greaterThan: IUnboundedLexeme = R.merge(
{
- evaluate: relationalEvaluator(([op, exp]) => op > exp),
+ evaluate: relationalEvaluator(([lhs, rhs, relOp]) =>
+ fnEval((l, r) => l > r, lhs, rhs, relOp)
+ ),
subType: RelationalOperator.GreaterThan,
- regexp: /^(>|(gt)(?=\s|$))/i,
+ regexp: /^((i|s)?(>|(gt)(?=\s|$)))/i,
+ regexpFlags: 2,
regexpMatch: 1
},
LEXEME_BASE
@@ -121,9 +152,12 @@ export const dateStartsWith: IUnboundedLexeme = R.merge(
export const lessOrEqual: IUnboundedLexeme = R.merge(
{
- evaluate: relationalEvaluator(([op, exp]) => op <= exp),
+ evaluate: relationalEvaluator(([lhs, rhs, relOp]) =>
+ fnEval((l, r) => l <= r, lhs, rhs, relOp)
+ ),
subType: RelationalOperator.LessOrEqual,
- regexp: /^(<=|(le)(?=\s|$))/i,
+ regexp: /^((i|s)?(<=|(le)(?=\s|$)))/i,
+ regexpFlags: 2,
regexpMatch: 1
},
LEXEME_BASE
@@ -131,9 +165,12 @@ export const lessOrEqual: IUnboundedLexeme = R.merge(
export const lessThan: IUnboundedLexeme = R.merge(
{
- evaluate: relationalEvaluator(([op, exp]) => op < exp),
+ evaluate: relationalEvaluator(([lhs, rhs, relOp]) =>
+ fnEval((l, r) => l < r, lhs, rhs, relOp)
+ ),
subType: RelationalOperator.LessThan,
- regexp: /^(<|(lt)(?=\s|$))/i,
+ regexp: /^((i|s)?(<|(lt)(?=\s|$)))/i,
+ regexpFlags: 2,
regexpMatch: 1
},
LEXEME_BASE
@@ -141,9 +178,12 @@ export const lessThan: IUnboundedLexeme = R.merge(
export const notEqual: IUnboundedLexeme = R.merge(
{
- evaluate: relationalEvaluator(([op, exp]) => op !== exp),
+ evaluate: relationalEvaluator(([lhs, rhs, relOp]) =>
+ fnEval((l, r) => l !== r, lhs, rhs, relOp)
+ ),
subType: RelationalOperator.NotEqual,
- regexp: /^(!=|(ne)(?=\s|$))/i,
+ regexp: /^((i|s)?(!=|(ne)(?=\s|$)))/i,
+ regexpFlags: 2,
regexpMatch: 1
},
LEXEME_BASE
diff --git a/tests/js-unit/dash_table_queries_test.ts b/tests/js-unit/dash_table_queries_test.ts
index 88799e2b5..9dd6b1354 100644
--- a/tests/js-unit/dash_table_queries_test.ts
+++ b/tests/js-unit/dash_table_queries_test.ts
@@ -265,6 +265,20 @@ describe('Dash Table Queries', () => {
target: {a: '\r\n1\r\n'},
valid: true,
evaluate: true
+ },
+ {
+ name: "compare 'abc' to 'ABC' (insensitive)",
+ query: `${c.hideOperand ? '' : '{a} '}ieq ABC`,
+ target: {a: 'abc'},
+ valid: true,
+ evaluate: true
+ },
+ {
+ name: "compare 'abc' to 'ABC' (sensitive)",
+ query: `${c.hideOperand ? '' : '{a} '}seq ABC`,
+ target: {a: 'abc'},
+ valid: true,
+ evaluate: false
}
]);
});
@@ -280,6 +294,15 @@ describe('Dash Table Queries', () => {
valid: true,
evaluate: true
},
+ {
+ name: 'insensitive compares "abc" to A',
+ query: `${
+ c.hideOperand ? '' : '{a} '
+ }icontains A`,
+ target: {a: 'abc'},
+ valid: true,
+ evaluate: true
+ },
{
name: 'cannot compare 11 to 1',
query: `${
@@ -370,6 +393,42 @@ describe('Dash Table Queries', () => {
target: {a: 'ab c'},
valid: true,
evaluate: true
+ },
+ {
+ name: "compare 'ab c' to 'B' (insensitive)",
+ query: `${
+ c.hideOperand ? '' : '{a} '
+ }icontains B`,
+ target: {a: 'ab c'},
+ valid: true,
+ evaluate: true
+ },
+ {
+ name: "compare 'ab c' to 'b' (insensitive)",
+ query: `${
+ c.hideOperand ? '' : '{a} '
+ }icontains b`,
+ target: {a: 'ab c'},
+ valid: true,
+ evaluate: true
+ },
+ {
+ name: "compare 'ab c' to 'B' (sensitive)",
+ query: `${
+ c.hideOperand ? '' : '{a} '
+ }scontains B`,
+ target: {a: 'ab c'},
+ valid: true,
+ evaluate: false
+ },
+ {
+ name: "compare 'ab c' to 'b' (sensitive)",
+ query: `${
+ c.hideOperand ? '' : '{a} '
+ }scontains b`,
+ target: {a: 'ab c'},
+ valid: true,
+ evaluate: true
}
]);
});
diff --git a/tests/js-unit/query_syntactic_tree_test.ts b/tests/js-unit/query_syntactic_tree_test.ts
index f867604e5..7cd56705f 100644
--- a/tests/js-unit/query_syntactic_tree_test.ts
+++ b/tests/js-unit/query_syntactic_tree_test.ts
@@ -646,6 +646,22 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(false);
});
+ it('can do equality (ieq) test', () => {
+ const tree = new QuerySyntaxTree('{a} ieq "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ });
+
+ it('can do equality (seq) test', () => {
+ const tree = new QuerySyntaxTree('{a} seq "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ });
+
it('can do equality (=) test', () => {
const tree = new QuerySyntaxTree('{a} = "1"');
@@ -656,6 +672,22 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(false);
});
+ it('can do equality (i=) test', () => {
+ const tree = new QuerySyntaxTree('{a} i= "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ });
+
+ it('can do equality (s=) test', () => {
+ const tree = new QuerySyntaxTree('{a} s= "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ });
+
it('can do difference (ne) test', () => {
const tree = new QuerySyntaxTree('{a} ne "1"');
@@ -666,6 +698,22 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(true);
});
+ it('can do difference (ine) test', () => {
+ const tree = new QuerySyntaxTree('{a} ine "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ });
+
+ it('can do difference (sne) test', () => {
+ const tree = new QuerySyntaxTree('{a} sne "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ });
+
it('can do difference (!=) test', () => {
const tree = new QuerySyntaxTree('{a} != "1"');
@@ -676,6 +724,22 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(true);
});
+ it('can do difference (i!=) test', () => {
+ const tree = new QuerySyntaxTree('{a} i!= "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ });
+
+ it('can do difference (s!=) test', () => {
+ const tree = new QuerySyntaxTree('{a} s!= "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ });
+
it('can do greater than (gt) test', () => {
const tree = new QuerySyntaxTree('{a} gt "1"');
@@ -686,6 +750,30 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(true);
});
+ it('can do greater than (igt) test', () => {
+ const tree = new QuerySyntaxTree('{a} igt "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(false);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ expect(tree.evaluate({a: 'abd'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(true);
+ });
+
+ it('can do greater than (sgt) test', () => {
+ const tree = new QuerySyntaxTree('{a} sgt "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(false);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ expect(tree.evaluate({a: 'abd'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(false);
+ });
+
it('can do greater than (>) test', () => {
const tree = new QuerySyntaxTree('{a} > "1"');
@@ -696,6 +784,30 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(true);
});
+ it('can do greater than (i>) test', () => {
+ const tree = new QuerySyntaxTree('{a} i> "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(false);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ expect(tree.evaluate({a: 'abd'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(true);
+ });
+
+ it('can do greater than (s>) test', () => {
+ const tree = new QuerySyntaxTree('{a} s> "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(false);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ expect(tree.evaluate({a: 'abd'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(false);
+ });
+
it('can do less than (lt) test', () => {
const tree = new QuerySyntaxTree('{a} lt "1"');
@@ -706,6 +818,30 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(false);
});
+ it('can do less than (igt) test', () => {
+ const tree = new QuerySyntaxTree('{a} ilt "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ expect(tree.evaluate({a: 'abd'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(false);
+ });
+
+ it('can do less than (sgt) test', () => {
+ const tree = new QuerySyntaxTree('{a} slt "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ expect(tree.evaluate({a: 'abd'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(true);
+ });
+
it('can do less than (<) test', () => {
const tree = new QuerySyntaxTree('{a} < "1"');
@@ -716,6 +852,30 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(false);
});
+ it('can do less than (i<) test', () => {
+ const tree = new QuerySyntaxTree('{a} i< "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ expect(tree.evaluate({a: 'abd'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(false);
+ });
+
+ it('can do less than (s<) test', () => {
+ const tree = new QuerySyntaxTree('{a} s< "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ expect(tree.evaluate({a: 'abd'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(true);
+ });
+
it('can do greater or equal to (ge) test', () => {
const tree = new QuerySyntaxTree('{a} ge "1"');
@@ -726,6 +886,30 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(true);
});
+ it('can do greater or equal to (ige) test', () => {
+ const tree = new QuerySyntaxTree('{a} ige "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(false);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ expect(tree.evaluate({a: 'abd'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(true);
+ });
+
+ it('can do greater or equal to (sge) test', () => {
+ const tree = new QuerySyntaxTree('{a} sge "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(false);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ expect(tree.evaluate({a: 'abd'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(false);
+ });
+
it('can do greater or equal to (>=) test', () => {
const tree = new QuerySyntaxTree('{a} >= "1"');
@@ -736,6 +920,30 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(true);
});
+ it('can do greater or equal to (i>=) test', () => {
+ const tree = new QuerySyntaxTree('{a} i>= "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(false);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ expect(tree.evaluate({a: 'abd'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(true);
+ });
+
+ it('can do greater or equal to (s>=) test', () => {
+ const tree = new QuerySyntaxTree('{a} s>= "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(false);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(false);
+ expect(tree.evaluate({a: 'abd'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(false);
+ });
+
it('can do less or equal to (le) test', () => {
const tree = new QuerySyntaxTree('{a} le "1"');
@@ -746,6 +954,30 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(false);
});
+ it('can do less or equal to (ile) test', () => {
+ const tree = new QuerySyntaxTree('{a} ile "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ expect(tree.evaluate({a: 'abd'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(false);
+ });
+
+ it('can do less or equal to (sle) test', () => {
+ const tree = new QuerySyntaxTree('{a} sle "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ expect(tree.evaluate({a: 'abd'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(true);
+ });
+
it('can do less or equal to (<=) test', () => {
const tree = new QuerySyntaxTree('{a} <= "1"');
@@ -756,6 +988,30 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate(data3)).to.equal(false);
});
+ it('can do less or equal to (i<=) test', () => {
+ const tree = new QuerySyntaxTree('{a} i<= "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ expect(tree.evaluate({a: 'abd'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(false);
+ });
+
+ it('can do less or equal to (s<=) test', () => {
+ const tree = new QuerySyntaxTree('{a} s<= "abc"');
+
+ expect(tree.isValid).to.equal(true);
+ expect(tree.evaluate({a: 'abb'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABB'})).to.equal(true);
+ expect(tree.evaluate({a: 'abc'})).to.equal(true);
+ expect(tree.evaluate({a: 'ABC'})).to.equal(true);
+ expect(tree.evaluate({a: 'abd'})).to.equal(false);
+ expect(tree.evaluate({a: 'ABD'})).to.equal(true);
+ });
+
it('can do contains (contains) test', () => {
const tree = new QuerySyntaxTree('{a} contains v');
@@ -764,6 +1020,24 @@ describe('Query Syntax Tree', () => {
expect(tree.evaluate({a: 'abc w'})).to.equal(false);
});
+ it('can do contains (icontains) test', () => {
+ const tree = new QuerySyntaxTree('{a} icontains 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);
+ expect(tree.evaluate({a: 'abc V'})).to.equal(true);
+ });
+
+ it('can do contains (scontains) test', () => {
+ const tree = new QuerySyntaxTree('{a} scontains 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);
+ expect(tree.evaluate({a: 'abc V'})).to.equal(false);
+ });
+
it('correctly interprets text-based with no spaces as invalid', () => {
const tree = new QuerySyntaxTree('{a} le5');
expect(tree.isValid).to.equal(false);
diff --git a/tests/selenium/conftest.py b/tests/selenium/conftest.py
index dc47f2ac4..e9403e315 100644
--- a/tests/selenium/conftest.py
+++ b/tests/selenium/conftest.py
@@ -1,3 +1,4 @@
+import platform
import pytest
from dash.testing.browser import Browser
@@ -260,6 +261,17 @@ def filter(self):
)
)
+ def filter_clear(self):
+ CMD = Keys.COMMAND if platform.system() == "Darwin" else Keys.CONTROL
+
+ self.filter().find_element_by_css_selector("input").click()
+ ac = ActionChains(self.mixin.driver)
+ ac.key_down(CMD)
+ ac.send_keys("a")
+ ac.key_up(CMD)
+ ac.send_keys(Keys.DELETE)
+ ac.perform()
+
def filter_click(self):
self.filter().click()
@@ -273,8 +285,10 @@ def filter_value(self, value=None):
.find_element_by_css_selector("input")
.get_attribute("value")
)
+ elif value == "":
+ self.filter_clear()
else:
- self.filter_click()
+ self.filter_clear()
self.mixin.driver.switch_to.active_element.send_keys(value + Keys.ENTER)
diff --git a/tests/selenium/test_derived_props.py b/tests/selenium/test_derived_props.py
index 95df9550c..fe057a7e8 100644
--- a/tests/selenium/test_derived_props.py
+++ b/tests/selenium/test_derived_props.py
@@ -51,6 +51,7 @@ def get_app():
row_deletable=True,
row_selectable=True,
sort_action="native",
+ style_cell=dict(width=100, min_width=100, max_width=100),
),
html.Div(id="props_container", children=["Nothing yet"]),
]
diff --git a/tests/selenium/test_filter.py b/tests/selenium/test_filter.py
index 88f97af9c..eeb746649 100644
--- a/tests/selenium/test_filter.py
+++ b/tests/selenium/test_filter.py
@@ -78,3 +78,226 @@ def test_filt001_basic(test, props, expect):
assert target.cell(index, "a").get_text() == value
assert test.get_log_errors() == []
+
+
+@pytest.mark.parametrize(
+ "filter_options,column_filter_options",
+ [
+ ("sensitive", None),
+ ("sensitive", None),
+ ("sensitive", None),
+ ("insensitive", None),
+ ("sensitive", "insensitive"),
+ ("insensitive", "sensitive"),
+ ],
+)
+def test_filt002_sensitivity(test, filter_options, column_filter_options):
+ props = dict(
+ id="table",
+ data=[dict(a="abc", b="abc", c="abc"), dict(a="ABC", b="ABC", c="ABC")],
+ columns=[
+ dict(
+ id="a",
+ name="a",
+ filter_options=dict(case=column_filter_options)
+ if column_filter_options is not None
+ else None,
+ type="any",
+ ),
+ dict(
+ id="b",
+ name="b",
+ filter_options=dict(case=column_filter_options)
+ if column_filter_options is not None
+ else None,
+ type="text",
+ ),
+ dict(
+ id="c",
+ name="c",
+ filter_options=dict(case=column_filter_options)
+ if column_filter_options is not None
+ else None,
+ type="numeric",
+ ),
+ ],
+ filter_action="native",
+ filter_options=dict(case=filter_options)
+ if filter_options is not None
+ else None,
+ style_cell=dict(width=100, min_width=100, max_width=100),
+ )
+
+ sensitivity = (
+ filter_options if column_filter_options is None else column_filter_options
+ )
+
+ test.start_server(get_app(props))
+
+ target = test.table("table")
+
+ # any -> implicit contains
+ target.column("a").filter_value("A")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "a").get_text() == "ABC"
+ assert not target.cell(1, "a").exists()
+ else:
+ assert target.cell(0, "a").get_text() == "abc"
+ assert target.cell(1, "a").get_text() == "ABC"
+
+ target.column("a").filter_value("a")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "a").get_text() == "abc"
+ assert not target.cell(1, "a").exists()
+ else:
+ assert target.cell(0, "a").get_text() == "abc"
+ assert target.cell(1, "a").get_text() == "ABC"
+
+ # text -> implicit contains
+ target.column("a").filter_value("")
+ target.column("b").filter_value("A")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "b").get_text() == "ABC"
+ assert not target.cell(1, "b").exists()
+ else:
+ assert target.cell(0, "b").get_text() == "abc"
+ assert target.cell(1, "b").get_text() == "ABC"
+
+ target.column("b").filter_value("a")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "b").get_text() == "abc"
+ assert not target.cell(1, "b").exists()
+ else:
+ assert target.cell(0, "b").get_text() == "abc"
+ assert target.cell(1, "b").get_text() == "ABC"
+
+ # numeric -> implicit equal
+ target.column("b").filter_value("")
+ target.column("c").filter_value("A")
+ assert not target.cell(0, "c").exists()
+
+ target.column("c").filter_value("a")
+ assert not target.cell(0, "c").exists()
+
+ target.column("c").filter_value("ABC")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "c").get_text() == "ABC"
+ assert not target.cell(1, "c").exists()
+ else:
+ assert target.cell(0, "c").get_text() == "abc"
+ assert target.cell(1, "c").get_text() == "ABC"
+
+ target.column("c").filter_value("abc")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "c").get_text() == "abc"
+ assert not target.cell(1, "c").exists()
+ else:
+ assert target.cell(0, "c").get_text() == "abc"
+ assert target.cell(1, "c").get_text() == "ABC"
+
+
+@pytest.mark.parametrize(
+ "filter_options,column_filter_options",
+ [
+ ("sensitive", None),
+ ("sensitive", None),
+ ("sensitive", None),
+ ("insensitive", None),
+ ("sensitive", "insensitive"),
+ ("insensitive", "sensitive"),
+ ],
+)
+def test_filt003_sensitivity(test, filter_options, column_filter_options):
+ props = dict(
+ id="table",
+ data=[dict(a="abc", b="abc", c="abc"), dict(a="ABC", b="ABC", c="ABC")],
+ columns=[
+ dict(
+ id="a",
+ name="a",
+ filter_options=dict(case=column_filter_options)
+ if column_filter_options is not None
+ else None,
+ type="any",
+ ),
+ dict(
+ id="b",
+ name="b",
+ filter_options=dict(case=column_filter_options)
+ if column_filter_options is not None
+ else None,
+ type="text",
+ ),
+ dict(
+ id="c",
+ name="c",
+ filter_options=dict(case=column_filter_options)
+ if column_filter_options is not None
+ else None,
+ type="numeric",
+ ),
+ ],
+ filter_action="native",
+ filter_options=dict(case=filter_options)
+ if filter_options is not None
+ else None,
+ style_cell=dict(width=100, min_width=100, max_width=100),
+ )
+
+ sensitivity = (
+ filter_options if column_filter_options is None else column_filter_options
+ )
+
+ test.start_server(get_app(props))
+
+ target = test.table("table")
+
+ target.column("a").filter_value("contains A")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "a").get_text() == "ABC"
+ assert not target.cell(1, "a").exists()
+ else:
+ assert target.cell(0, "a").get_text() == "abc"
+ assert target.cell(1, "a").get_text() == "ABC"
+
+ target.column("a").filter_value("contains a")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "a").get_text() == "abc"
+ assert not target.cell(1, "a").exists()
+ else:
+ assert target.cell(0, "a").get_text() == "abc"
+ assert target.cell(1, "a").get_text() == "ABC"
+
+ target.column("a").filter_value("")
+ target.column("b").filter_value("contains A")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "b").get_text() == "ABC"
+ assert not target.cell(1, "b").exists()
+ else:
+ assert target.cell(0, "b").get_text() == "abc"
+ assert target.cell(1, "b").get_text() == "ABC"
+
+ target.column("b").filter_value("contains a")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "b").get_text() == "abc"
+ assert not target.cell(1, "b").exists()
+ else:
+ assert target.cell(0, "b").get_text() == "abc"
+ assert target.cell(1, "b").get_text() == "ABC"
+
+ target.column("b").filter_value("")
+ target.column("c").filter_value("contains A")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "c").get_text() == "ABC"
+ assert not target.cell(1, "c").exists()
+ else:
+ assert target.cell(0, "c").get_text() == "abc"
+ assert target.cell(1, "c").get_text() == "ABC"
+
+ target.column("c").filter_value("contains a")
+ if sensitivity == "sensitive":
+ assert target.cell(0, "c").get_text() == "abc"
+ assert not target.cell(1, "c").exists()
+ else:
+ assert target.cell(0, "c").get_text() == "abc"
+ assert target.cell(1, "c").get_text() == "ABC"