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

Issue 545 - Case-insensitive option for filters #609

Closed
wants to merge 13 commits into from
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ column filters not working.

## [4.4.0] - 2019-10-08
### Added
[#545](https://github.com/plotly/dash-table/issues/545)
- Case insensitive filtering
- New props: `filter_case` - to control case of all filters, `columns.filter_case` - to control filter case for each column
- New operators: `i=`, `i>=`, `i>`, `i<=`, `i<`, `i!=`, `icontains` - for case-insensitive filtering, `s=`, `s>=`, `s>`, `s<=`, `s<`, `s!=`, `scontains` - to force case-sensitive filtering on case-insensitive columns

[#546](https://github.com/plotly/dash-table/issues/546)
- New prop `export_columns` that takes values `all` or `visible` (default). This prop controls the columns used during export

Expand Down
18 changes: 16 additions & 2 deletions demo/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Logger from 'core/Logger';
import AppState, { AppMode, AppFlavor } from './AppMode';

import './style.less';
import FilterCaseButton from 'dash-table/components/Filter/FilterCaseButton';
import { Case } from 'dash-table/components/Table/props';

class App extends Component<any, any> {
constructor(props: any) {
Expand All @@ -25,7 +27,7 @@ class App extends Component<any, any> {
const flavors = flavorParam ? flavorParam.split(';') : [];

if (flavors.indexOf(AppFlavor.FilterNative) !== -1) {
return (<div>
return (<div className='demo-app-root'>
<button
className='clear-filters'
onClick={() => {
Expand All @@ -35,6 +37,17 @@ class App extends Component<any, any> {
this.setState({ tableProps });
}}
>Clear Filter</button>
<FilterCaseButton
filterCase={this.state.tableProps.filter_case === Case.Insensitive
? Case.Insensitive : Case.Sensitive}
setColumnCase={() => {
const tableProps = R.clone(this.state.tableProps);
tableProps.filter_case = tableProps.filter_case === Case.Insensitive
? Case.Sensitive : Case.Insensitive;

this.setState({ tableProps });
}}
/>
<input
style={{ width: '500px' }}
value={this.state.temp_filtering}
Expand All @@ -47,6 +60,7 @@ class App extends Component<any, any> {

this.setState({ tableProps });
}} />

</div>);
} else if (mode === AppMode.TaleOfTwoTables) {
if (!this.state.tableProps2) {
Expand Down Expand Up @@ -78,7 +92,7 @@ class App extends Component<any, any> {
return (newProps: any) => {
Logger.debug('--->', newProps);
this.setState((prevState: any) => ({
tableProps: R.merge(prevState.tableProps, newProps)
tableProps: R.mergeRight(prevState.tableProps, newProps)
}));
};
});
Expand Down
6 changes: 3 additions & 3 deletions demo/AppMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const BasicModes = [
function getBaseTableProps(mock: IDataMock): Partial<IProps> {
return {
id: 'table',
columns: mock.columns.map((col: any) => R.merge(col, {
columns: mock.columns.map((col: any) => R.mergeRight(col, {
name: col.name || col.id,
on_change: {
action: ChangeAction.None
Expand Down Expand Up @@ -108,7 +108,7 @@ function getDefaultState(

return {
filter_query: '',
tableProps: R.merge(getBaseTableProps(mock), {
tableProps: R.mergeRight(getBaseTableProps(mock), {
data: mock.data,
editable: true,
sort_action: TableAction.Native,
Expand Down Expand Up @@ -271,7 +271,7 @@ function getVirtualizedState() {

return {
filter_query: '',
tableProps: R.merge(getBaseTableProps(mock), {
tableProps: R.mergeRight(getBaseTableProps(mock), {
data: mock.data,
editable: true,
fill_width: false,
Expand Down
15 changes: 14 additions & 1 deletion demo/style.less
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
html {
font-size: 13px;
}

.demo-app-root {
input.dash-filter--case {
outline: none;
height: 18px;
}

input.dash-filter--case--sensitive {
border-color: hotpink;
border-radius: 4px;
border-width: 2px;
}
}
}
4 changes: 3 additions & 1 deletion src/core/syntax-tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface IStructure {
subType?: string;
type: string;
value: any;
case: string | undefined;

block?: IStructure;
left?: IStructure;
Expand All @@ -21,7 +22,8 @@ function toStructure(tree: ISyntaxTree): IStructure {
const res: IStructure = {
subType: lexeme.subType,
type: lexeme.type,
value: lexeme.present ? lexeme.present(tree) : value
value: lexeme.present ? lexeme.present(tree) : value,
case: lexeme.case
};

if (block) {
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 @@ -16,6 +16,7 @@ export interface IUnboundedLexeme {
resolve?: (target: any, tree: ISyntaxTree) => any;
subType?: string;
type: string;
case?: string;
nesting?: number;
priority?: number;
regexp: RegExp;
Expand Down
2 changes: 1 addition & 1 deletion src/dash-table/components/CellFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export default class CellFactory {
) => {
return React.cloneElement(wrapper, {
children: [content],
style: R.merge(style, { borderBottom, borderLeft, borderRight, borderTop })
style: R.mergeRight(style, { borderBottom, borderLeft, borderRight, borderTop })
});
});

Expand Down
74 changes: 52 additions & 22 deletions src/dash-table/components/Filter/Column.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,78 @@
import * as R from 'ramda';
import React, { CSSProperties, PureComponent } from 'react';

import IsolatedInput from 'core/components/IsolatedInput';
import FilterCaseButton from './FilterCaseButton';

import { ColumnId } from 'dash-table/components/Table/props';
import { Case, SetProps, IColumn } from 'dash-table/components/Table/props';
import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper';

type SetFilter = (ev: any) => void;
type SetFilter = (ev: any, c: Case, column: IColumn) => void;

interface IColumnFilterProps {
classes: string;
columnId: ColumnId;
column: IColumn;
columns: IColumn[];
isValid: boolean;
setFilter: SetFilter;
setProps: SetProps;
style?: CSSProperties;
value?: string;
globalFilterCase: Case;
columnFilterCase: Case;
}

interface IState {
value?: string;
}

export default class ColumnFilter extends PureComponent<IColumnFilterProps, IState> {
constructor(props: IColumnFilterProps) {
super(props);
export default class ColumnFilter extends PureComponent<IColumnFilterProps> {
private submit = (value: string | undefined) => {
const { column, setFilter, columnFilterCase } = this.props;

this.state = {
value: props.value
};
setFilter(
column,
this.getComputedCase(columnFilterCase),
value as any);
}

private submit = (value: string | undefined) => {
const { setFilter } = this.props;
private setColumnCase = () => {
const { columns, column, setFilter, columnFilterCase, globalFilterCase, setProps, value } = this.props;

setFilter({
target: { value }
} as any);
const cols: IColumn[] = R.clone(columns);
const inx: number = R.findIndex(R.propEq('id', column.id))(cols);

const newColumnFilterCase = globalFilterCase === Case.Sensitive
? ((columnFilterCase === Case.Sensitive || columnFilterCase === Case.Default)
? Case.Insensitive : Case.Default)
: ((columnFilterCase === Case.Insensitive || columnFilterCase === Case.Default)
? Case.Sensitive : Case.Default);

const newComputedCase = this.getComputedCase(newColumnFilterCase);

cols[inx].filter_case = newColumnFilterCase;

setFilter(
cols[inx],
newComputedCase,
value || '' as any);
setProps({ columns: cols });
}

private getComputedCase = (columnFilterCase: Case) =>
(columnFilterCase === Case.Insensitive ||
(this.props.globalFilterCase === Case.Insensitive && columnFilterCase !== Case.Sensitive))
? Case.Insensitive : Case.Sensitive

render() {
const {
classes,
columnId,
column,
isValid,
style,
value
value,
columnFilterCase
} = this.props;

return (<th
className={classes + (isValid ? '' : ' invalid')}
data-dash-column={columnId}
data-dash-column={column.id}
style={style}
>
<IsolatedInput
Expand All @@ -60,10 +84,16 @@ export default class ColumnFilter extends PureComponent<IColumnFilterProps, ISta
e.stopPropagation();
}}
value={value}
placeholder={`filter data...`}
placeholder='filter data...'
stopPropagation={true}
submit={this.submit}
/>
<div>
<FilterCaseButton
filterCase={this.getComputedCase(columnFilterCase)}
setColumnCase={this.setColumnCase}
/>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Had to make this an input type button, because there was no other way to make this thing have cursor: pointer, it would still be text from input below it. Setting z-index did nothing.

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

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

interface IFilterCaseButtonProps {
filterCase: Case;
setColumnCase: () => void;
}

export default class FilterCaseButton extends PureComponent<IFilterCaseButtonProps> {
render() {
const filterCaseClass: string = (this.props.filterCase !== Case.Insensitive) ?
'dash-filter--case--sensitive' : 'dash-filter--case--insensitive';

return (<input
type='button'
className={'dash-filter--case ' + filterCaseClass}
onClick={this.props.setColumnCase}
value='Aa'
/>);
}
}
36 changes: 25 additions & 11 deletions src/dash-table/components/FilterFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import memoizerCache from 'core/cache/memoizer';
import { memoizeOne } from 'core/memoizer';

import ColumnFilter from 'dash-table/components/Filter/Column';
import { ColumnId, IColumn, TableAction, IFilterFactoryProps, SetFilter } from 'dash-table/components/Table/props';
import { ColumnId, IColumn, TableAction, IFilterFactoryProps, SetFilter, Case, SetProps } from 'dash-table/components/Table/props';
import derivedFilterStyles, { derivedFilterOpStyles } from 'dash-table/derived/filter/wrapperStyles';
import derivedHeaderOperations from 'dash-table/derived/header/operations';
import { derivedRelevantFilterStyles } from 'dash-table/derived/style';
Expand All @@ -32,29 +32,37 @@ export default class FilterFactory {

}

private onChange = (column: IColumn, map: Map<string, SingleColumnSyntaxTree>, setFilter: SetFilter, ev: any) => {
Logger.debug('Filter -- onChange', column.id, ev.target.value && ev.target.value.trim());
private onChange =
(map: Map<string, SingleColumnSyntaxTree>, setFilter: SetFilter, column: IColumn, computed_filter_case: Case, ev: any) => {
Logger.debug('Filter -- onChange', column.id, ev && ev.trim());

const value = ev.target.value.trim();
const value = ev && ev.trim();

updateColumnFilter(map, column, value, setFilter);
updateColumnFilter(map, column, value, setFilter, computed_filter_case);
}

private filter = memoizerCache<[ColumnId, number]>()((
column: IColumn,
columns: IColumn[],
index: number,
map: Map<string, SingleColumnSyntaxTree>,
setFilter: SetFilter
setFilter: SetFilter,
setProps: SetProps,
filter_case: Case
) => {
const ast = map.get(column.id.toString());

return (<ColumnFilter
key={`column-${index}`}
classes={`dash-filter column-${index}`}
columnId={column.id}
column={column}
columns={columns}
isValid={!ast || ast.isValid}
setFilter={this.onChange.bind(this, column, map, setFilter)}
setFilter={this.onChange.bind(this, map, setFilter)}
setProps={setProps}
value={ast && ast.query}
globalFilterCase={filter_case || Case.Default}
columnFilterCase={column.filter_case || Case.Default}
/>);
});

Expand All @@ -63,7 +71,7 @@ export default class FilterFactory {
edges: IEdgesMatrices | undefined
) => arrayMap(
styles,
(s, j) => R.merge(
(s, j) => R.mergeRight(
s,
edges && edges.getStyle(0, j)
)
Expand All @@ -74,16 +82,19 @@ export default class FilterFactory {
filterOpEdges: IEdgesMatrices | undefined
) {
const {
columns,
filter_action,
map,
row_deletable,
row_selectable,
setFilter,
setProps,
style_cell,
style_cell_conditional,
style_filter,
style_filter_conditional,
visibleColumns
visibleColumns,
filter_case
} = this.props;

if (filter_action === TableAction.None) {
Expand Down Expand Up @@ -111,9 +122,12 @@ export default class FilterFactory {
const filters = R.addIndex<IColumn, JSX.Element>(R.map)((column, index) => {
return this.filter.get(column.id, index)(
column,
columns,
index,
map,
setFilter
setFilter,
setProps,
filter_case
);
}, visibleColumns);

Expand Down
Loading