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

Issue 545 - Case (in)sensitive filtering #893

Merged
merged 17 commits into from
May 11, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .eslintrc → .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
var path = require('path');

module.exports = {
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier"
Expand All @@ -7,7 +9,7 @@
"@typescript-eslint"
],
"parserOptions": {
"project": "./tsconfig.lint.json"
"project": path.join(__dirname, "tsconfig.lint.json"),
},
"rules": {
"arrow-parens": [
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/core/syntax-tree/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface ILexerResult {

export interface ILexemeResult {
lexeme: ILexeme;
flags?: string;
value?: string;
}

Expand Down Expand Up @@ -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});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a lexicon perspective treat i|s prefix to applicable relational operators as "flags" that can be used to determine the exact processing to do during evaluation. value is the "base" operator + flags (e.g. icontains)


query = query.substring(value.length);
}
Expand Down
1 change: 1 addition & 0 deletions src/core/syntax-tree/lexicon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 17 additions & 2 deletions src/dash-table/components/Filter/Column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, FilterCase} 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: FilterCase;
isValid: boolean;
setFilter: SetFilter;
style?: CSSProperties;
toggleFilterOptions: () => void;
value?: string;
}

Expand Down Expand Up @@ -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 (
<th
Expand All @@ -62,6 +73,10 @@ export default class ColumnFilter extends PureComponent<
stopPropagation={true}
submit={this.submit}
/>
<FilterOptions
filterOptions={filterOptions}
toggleFilterOptions={toggleFilterOptions}
/>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New component displayed in the filter cell to toggle filtering options

</th>
);
}
Expand Down
21 changes: 21 additions & 0 deletions src/dash-table/components/Filter/FilterOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';

import {FilterCase} from 'dash-table/components/Table/props';

interface IFilterCaseButtonProps {
filterOptions: FilterCase;
toggleFilterOptions: () => void;
}

export default ({
filterOptions,
toggleFilterOptions
}: IFilterCaseButtonProps) => (<input
type='button'
className={`dash-filter--case ${filterOptions === FilterCase.Sensitive
? 'dash-filter--case--sensitive'
: 'dash-filter--case--insensitive'
}`}
onClick={toggleFilterOptions}
value='Aa'
/>);
26 changes: 24 additions & 2 deletions src/dash-table/components/FilterFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,27 @@ export default class FilterFactory {
updateColumnFilter(map, column, operator, value, setFilter);
};

private onToggleChange = (
column: IColumn,
map: Map<string, SingleColumnSyntaxTree>,
operator: FilterLogicalOperator,
setFilter: SetFilter,
toggleFilterOptions: (column: IColumn) => IColumn,
value: any
) => {
const newColumn = toggleFilterOptions(column);

updateColumnFilter(map, newColumn, operator, value, setFilter);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trigger a setProps and return the new column configuration, use the new column configuration to update the column's query (and trigger a refresh)

};

private filter = memoizerCache<[ColumnId, number]>()(
(
column: IColumn,
index: number,
map: Map<string, SingleColumnSyntaxTree>,
operator: FilterLogicalOperator,
setFilter: SetFilter
setFilter: SetFilter,
toggleFilterOptions: (column: IColumn) => IColumn
) => {
const ast = map.get(column.id.toString());

Expand All @@ -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,
Expand All @@ -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}
/>
);
Expand Down Expand Up @@ -107,6 +127,7 @@ export default class FilterFactory {
style_cell_conditional,
style_filter,
style_filter_conditional,
toggleFilterOptions,
visibleColumns
} = this.props;

Expand Down Expand Up @@ -139,7 +160,8 @@ export default class FilterFactory {
index,
map,
filter_action.operator,
setFilter
setFilter,
toggleFilterOptions
);
},
visibleColumns
Expand Down
34 changes: 29 additions & 5 deletions src/dash-table/components/Table/Table.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand Down Expand Up @@ -670,7 +687,7 @@
}

.dash-spreadsheet-inner {
[class^='column-header--'] {
[class^='column-header--'], [class^='dash-filter--'] {
cursor: pointer;
float: left;
}
Expand All @@ -685,6 +702,7 @@
}


.dash-filter--case,
.column-header--clear,
.column-header--delete,
.column-header--edit,
Expand All @@ -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;
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/dash-table/components/Table/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export enum ExportHeaders {
Display = 'display'
}

export enum FilterCase {
Insensitive = 'insensitive',
Sensitive = 'sensitive'
}

export enum SortMode {
Single = 'single',
Multi = 'multi'
Expand Down Expand Up @@ -193,6 +198,7 @@ export interface IBaseColumn {
clearable?: boolean | boolean[] | 'first' | 'last';
deletable?: boolean | boolean[] | 'first' | 'last';
editable: boolean;
filter_options: FilterCase;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FilterCase vs. filter_options may look inconsistent but is intended to be that way. In the future you may also want to add locale-aware comparisons and leaving some space to do so at a future time.

hideable?: boolean | boolean[] | 'first' | 'last';
renamable?: boolean | boolean[] | 'first' | 'last';
selectable?: boolean | boolean[] | 'first' | 'last';
Expand Down Expand Up @@ -327,6 +333,7 @@ export interface IProps {
data?: Data;
editable?: boolean;
fill_width?: boolean;
filter_options?: FilterCase;
filter_query?: string;
filter_action?: TableAction | IFilterAction;
hidden_columns?: string[];
Expand Down Expand Up @@ -381,6 +388,7 @@ interface IDefaultProps {
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
filter_options: FilterCase;
filter_query: string;
filter_action: TableAction;
fixed_columns: Fixed;
Expand Down Expand Up @@ -492,6 +500,7 @@ export interface IFilterFactoryProps {
style_cell_conditional: Cells;
style_filter: Style;
style_filter_conditional: BasicFilters;
toggleFilterOptions: (column: IColumn) => IColumn;
visibleColumns: Columns;
}

Expand Down
24 changes: 24 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import {asyncDecorator} from '@plotly/dash-component-plugins';

import LazyLoader from 'dash-table/LazyLoader';
import {FilterCase} from 'dash-table/components/Table/props';

/**
* Dash DataTable is an interactive table component designed for
Expand Down Expand Up @@ -56,6 +57,7 @@ export const defaultProps = {
dropdown_data: [],

fill_width: true,
filter_options: FilterCase.Sensitive,
fixed_columns: {
headers: false,
data: 0
Expand Down Expand Up @@ -181,6 +183,17 @@ 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.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
Expand Down Expand Up @@ -1142,6 +1155,17 @@ 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.oneOf(['sensitive', 'insensitive']),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a bit of a slack convo with @Marc-Andre-Rivet I think we concluded we should change this to filter_options: {case: ('sensitive'|'insensitive')}. The reason he wrote it as filter_options instead of something more specific like filter_case was to allow extending to ignoring other distinctions, like accents and punctuation. But that means the current syntax is confusing as it doesn't tell you what you're controlling the sensitivity to, and to extend later we'll have to support both these strings and objects like {case: 'sensitive', accents: 'insensitive', punctuation: 'sensitive'}

The alternative would be to make a single specific prop now, like filter_case: 'sensitive', and then add more props later filter_accents: 'sensitive', filter_punctuation: 'sensitive'. That's a little easier for the most common use of enabling case-insensitive filters, but will ultimately create more props than it would need to, at both the table and column levels.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing this to an object works for me. I knew @Marc-Andre-Rivet wrote this with extensibility in mind and I agree this more cleanly allows for various filter-option combinations

Copy link
Contributor Author

@Marc-Andre-Rivet Marc-Andre-Rivet May 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ada8456, 6f012c3, 4697136 (mistakes were made..)


/**
* The `sort_action` property enables data to be
* sorted on a per-column basis.
Expand Down
10 changes: 7 additions & 3 deletions src/dash-table/dash/Sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
ExportHeaders,
IFilterAction,
FilterLogicalOperator,
SelectedCells
SelectedCells,
FilterCase
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
Expand Down Expand Up @@ -63,11 +64,13 @@ const applyDefaultsToColumns = (
defaultLocale: INumberLocale,
defaultSort: SortAsNull,
columns: Columns,
editable: boolean
editable: boolean,
filterCase: FilterCase
) =>
R.map(column => {
const c = R.clone(column);
c.editable = resolveFlag(editable, column.editable);
c.filter_options = resolveFlag(filterCase, column.filter_options);
c.sort_as_null = c.sort_as_null || defaultSort;

if (c.type === ColumnType.Numeric && c.format) {
Expand Down Expand Up @@ -105,7 +108,8 @@ export default class Sanitizer {
locale_format,
props.sort_as_null,
props.columns,
props.editable
props.editable,
props.filter_options
)
: [];
const data = props.data ?? [];
Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/derived/cell/resolveFlag.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export default (tableFlag: boolean, columnFlag: boolean | undefined): boolean =>
export default <T>(tableFlag: T, columnFlag: T | undefined): T =>
columnFlag === undefined ? tableFlag : columnFlag;
Loading