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

Conversation

Marc-Andre-Rivet
Copy link
Contributor

@Marc-Andre-Rivet Marc-Andre-Rivet commented Apr 29, 2021

Closes #545. Uses as as a starting point and supersedes #609.

@wbrgss What follows is not necessary for the review itself but, if you want to dig a little bit into the table's query parser / AST implementation, here's the key things to look at:

@Marc-Andre-Rivet Marc-Andre-Rivet requested a review from wbrgss April 29, 2021 00:36
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)

<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

@@ -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.

) => {
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)

}

export default (propsFn: () => ControlledTableProps) => {
const setFilter = memoizeOne((setProps: SetProps, setState: SetState) =>
handleSetFilter.bind(undefined, setProps, setState)
);

const toggleFilterOptions = memoizeOne(
(setProps: SetProps, columns: IColumn[]) => (column: IColumn) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Called by https://github.com/plotly/dash-table/pull/893/files#r622661715

Defined at the factory level so that the individual components remain unaware of columns for memoization/cache busting / performance reasons and setProps so they remain unaware of Dash's idiosyncracies.


setProps({columns: newColumns});

return newColumn;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

update the table's columns in the shallowest way possible (minimize cache busting)

): boolean =>
relOp[0] == 'i'
? fn(lhs.toString().toUpperCase(), rhs.toString().toUpperCase())
: fn(lhs, rhs);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Conditional evaluation of contains, =, !=, >=, >, <=, < based on the flags

),
subType: RelationalOperator.Contains,
regexp: /^((contains)(?=\s|$))/i,
regexp: /^((i|s)?contains)(?=\s|$)/i,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

allow for optional flags

),
subType: RelationalOperator.Contains,
regexp: /^((contains)(?=\s|$))/i,
regexp: /^((i|s)?contains)(?=\s|$)/i,
regexpFlags: 2,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

),
subType: RelationalOperator.Equal,
regexp: /^(=|(eq)(?=\s|$))/i,
regexp: /^((i|s)?(=|(eq)(?=\s|$)))/i,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

same thing for all applicable relational operators

query: `${c.hideOperand ? '' : '{a} '}seq ABC`,
target: {a: 'abc'},
valid: true,
evaluate: false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Multiple new tests for the (in)sensitive versions of the operators -- as used by the table itself

expect(tree.isValid).to.equal(true);
expect(tree.evaluate({a: 'abc'})).to.equal(true);
expect(tree.evaluate({a: 'ABC'})).to.equal(false);
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test all new operators with the various query flavors

Copy link
Contributor

@wbrgss wbrgss left a comment

Choose a reason for hiding this comment

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

Looking nice so far! I'll probably do another more fine-grained pass of the code later. The functionality looks good.

If you have time @alexcjohnson I'd welcome a review from you as well

@Marc-Andre-Rivet Marc-Andre-Rivet marked this pull request as ready for review April 30, 2021 23:47
Copy link
Contributor

@wbrgss wbrgss left a comment

Choose a reason for hiding this comment

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

@Marc-Andre-Rivet Let's say I set a column to be case-insensitive using the toggle, or with column-level 'filter_options': 'insensitive'. I know I can force case-insensitive comparison using e.g. ilt, but I might also expect lt to behave the same as ilt, considering the case-insensitive toggle is on, and right now it doesn't on my end. Does that make sense?

filtering

@Marc-Andre-Rivet
Copy link
Contributor Author

@wbrgss Good point. Yes it makes total sense to have lt like ilt if the toggle is turned on. I'll see about adding that in.

Copy link
Contributor

@wbrgss wbrgss left a comment

Choose a reason for hiding this comment

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

Very nice @Marc-Andre-Rivet

💃

* 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..)

Marc-André Rivet added 3 commits May 11, 2021 12:07
…tring to improve forward compatibility and consistency
- invert column and table filter_options priority (column takes precedence)
Copy link
Collaborator

@alexcjohnson alexcjohnson left a comment

Choose a reason for hiding this comment

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

💃

@Marc-Andre-Rivet Marc-Andre-Rivet merged commit 776484f into dev May 11, 2021
@Marc-Andre-Rivet Marc-Andre-Rivet deleted the case-insensitive-redux branch May 11, 2021 18:24
@AnnMarieW
Copy link
Contributor

This is so awesome! 🎊

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

case insensitive filtering
4 participants