Skip to content

Commit

Permalink
feat(filters): add possibility to filter by text range like "a..e" (#279
Browse files Browse the repository at this point in the history
)

* feat(filters): add possibility to filter by text range like "a..f"

* chore(deps): update to latest multiple-select to fix issue found
- add more variable undefined checks in multiple-select external lib
- some users were hidding column(s) that had filter(s) on them and the multiple-select lib was throwing some errors and blocking the user from going further
  • Loading branch information
ghiscoding authored Mar 8, 2021
1 parent ca0c567 commit e44145d
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 45 deletions.
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"jquery": "^3.5.1",
"jquery-ui-dist": "^1.12.1",
"moment-mini": "^2.24.0",
"multiple-select-modified": "^1.3.10",
"multiple-select-modified": "^1.3.11",
"slickgrid": "^2.4.33"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ describe('getParsedSearchTermsByFieldType method', () => {
const input = 'world';
const result = getParsedSearchTermsByFieldType([input], 'string');

expect(result).toBe(input);
expect(result).toEqual([input]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,46 @@ describe('executeStringFilterCondition method', () => {
const output = executeStringFilterCondition(options, getFilterParsedText(searchTerms));
expect(output).toBe(true);
});

it('should return True when input value is in the range of search terms using 2 dots (..) notation', () => {
const searchTerms = ['b..e'];
const options = { dataKey: '', operator: 'EQ', cellValue: 'c', fieldType: FieldType.string, searchTerms } as FilterConditionOption;
const output = executeStringFilterCondition(options, getFilterParsedText(searchTerms));
expect(output).toBe(true);
});

it('should return True when input value is on the inclusive limit range of search terms using 2 dots (..) notation AND no operator provided except a defaultFilterRangeOperator is rangeInclusive', () => {
const searchTerms = ['b..e'];
const options = { dataKey: '', defaultFilterRangeOperator: OperatorType.rangeInclusive, cellValue: 'b', fieldType: FieldType.string, searchTerms } as FilterConditionOption;
const output = executeStringFilterCondition(options, getFilterParsedText(searchTerms));
expect(output).toBe(true);
});

it('should return False when input value is on the inclusive limit range of search terms using 2 dots (..) notation AND no operator provided except a defaultFilterRangeOperator is rangeExclusive', () => {
const searchTerms = ['b..e'];
const options = { dataKey: '', defaultFilterRangeOperator: OperatorType.rangeExclusive, cellValue: 'b', fieldType: FieldType.string, searchTerms } as FilterConditionOption;
const output = executeStringFilterCondition(options, getFilterParsedText(searchTerms));
expect(output).toBe(false);
});

it('should return False when input value is not in the range of search terms using 2 dots (..) notation', () => {
const searchTerms = ['b..e'];
const options = { dataKey: '', operator: 'EQ', cellValue: 'g', fieldType: FieldType.string, searchTerms } as FilterConditionOption;
const output = executeStringFilterCondition(options, getFilterParsedText(searchTerms));
expect(output).toBe(false);
});

it('should return True when input value equals the search terms min inclusive value and operator is set to "rangeInclusive" using 2 dots (..) notation', () => {
const searchTerms = ['b..e'];
const options = { dataKey: '', operator: 'RangeInclusive', cellValue: 'b', fieldType: FieldType.string, searchTerms } as FilterConditionOption;
const output = executeStringFilterCondition(options, getFilterParsedText(searchTerms));
expect(output).toBe(true);
});

it('should return False when input value equals the search terms min inclusive value and operator is set to "RangeExclusive" using 2 dots (..) notation', () => {
const searchTerms = ['b..e'];
const options = { dataKey: '', operator: 'RangeExclusive', cellValue: 'b', fieldType: FieldType.string, searchTerms } as FilterConditionOption;
const output = executeStringFilterCondition(options, getFilterParsedText(searchTerms));
expect(output).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const executeFilterConditionTest: FilterCondition = ((options: FilterCond
case 'text':
default:
// the parsedSearchTerms should be single value (result came from getFilterParsedText() method)
return executeStringFilterCondition(options, parsedSearchTerms as SearchTerm);
return executeStringFilterCondition(options, (parsedSearchTerms || []) as string[]);
}
}) as FilterCondition;

Expand Down Expand Up @@ -68,7 +68,7 @@ export function getParsedSearchTermsByFieldType(inputSearchTerms: SearchTerm[] |
parsedSearchValues = getFilterParsedObjectResult(inputSearchTerms);
break;
case 'text':
parsedSearchValues = getFilterParsedText(inputSearchTerms);
parsedSearchValues = getFilterParsedText(inputSearchTerms) as SearchTerm[];
break;
}
return parsedSearchValues;
Expand Down
74 changes: 56 additions & 18 deletions packages/common/src/filter-conditions/stringFilterCondition.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { OperatorType, SearchTerm } from '../enums/index';
import { OperatorString, OperatorType, SearchTerm } from '../enums/index';
import { FilterCondition, FilterConditionOption } from '../interfaces/index';
import { testFilterCondition } from './filterUtilities';

/** Execute filter condition check on each cell */
export const executeStringFilterCondition: FilterCondition = ((options: FilterConditionOption, parsedSearchValue: string | undefined) => {
if (parsedSearchValue === undefined && !options.operator) {
export const executeStringFilterCondition: FilterCondition = ((options: FilterConditionOption, parsedSearchValues: string[]) => {
let [searchValue1, searchValue2] = parsedSearchValues;

if (searchValue1 === undefined && !options.operator) {
return true;
}

Expand All @@ -13,28 +15,64 @@ export const executeStringFilterCondition: FilterCondition = ((options: FilterCo

// make both the cell value and search value lower for case insensitive comparison
const cellValue = options.cellValue.toLowerCase();
if (typeof parsedSearchValue === 'string') {
parsedSearchValue = parsedSearchValue.toLowerCase();
if (typeof searchValue1 === 'string') {
searchValue1 = searchValue1.toLowerCase();
}
if (typeof searchValue2 === 'string') {
searchValue2 = searchValue2.toLowerCase();
}

if (options.operator === '*' || options.operator === OperatorType.endsWith) {
return cellValue.endsWith(parsedSearchValue);
} else if ((options.operator === '' && options.searchInputLastChar === '*') || options.operator === OperatorType.startsWith) {
return cellValue.startsWith(parsedSearchValue);
} else if (options.operator === '' || options.operator === OperatorType.contains) {
return (cellValue.indexOf(parsedSearchValue) > -1);
} else if (options.operator === '<>' || options.operator === OperatorType.notContains) {
return (cellValue.indexOf(parsedSearchValue) === -1);
if (searchValue1 !== undefined && searchValue2 !== undefined) {
let operator = options?.operator ?? options.defaultFilterRangeOperator;
if (operator !== OperatorType.rangeInclusive && operator !== OperatorType.rangeExclusive) {
operator = options.defaultFilterRangeOperator;
}
const isInclusive = operator === OperatorType.rangeInclusive;
const searchResult1 = testStringCondition((isInclusive ? '>=' : '>'), cellValue, searchValue1, options.searchInputLastChar);
const searchResult2 = testStringCondition((isInclusive ? '<=' : '<'), cellValue, searchValue2, options.searchInputLastChar);
return searchResult1 && searchResult2;
}
return testFilterCondition(options.operator || '==', cellValue, parsedSearchValue);
const searchResult1 = testStringCondition(options.operator, cellValue, searchValue1, options.searchInputLastChar);
return searchResult1;
}) as FilterCondition;

/**
* From our search filter value(s), get the parsed value(s).
* This is called only once per filter before running the actual filter condition check on each cell
*/
export function getFilterParsedText(inputSearchTerms: SearchTerm[] | undefined): SearchTerm {
let parsedSearchValue = (Array.isArray(inputSearchTerms) && inputSearchTerms.length > 0) ? inputSearchTerms[0] : '';
parsedSearchValue = parsedSearchValue === undefined || parsedSearchValue === null ? '' : `${parsedSearchValue}`; // make sure it's a string
return parsedSearchValue;
export function getFilterParsedText(inputSearchTerms: SearchTerm[] | undefined): SearchTerm[] {
const defaultSearchTerm = ''; // when nothing is provided, we'll default to 0
const searchTerms = Array.isArray(inputSearchTerms) && inputSearchTerms || [defaultSearchTerm];
const parsedSearchValues: string[] = [];
let searchValue1;
let searchValue2;
if (searchTerms.length === 2 || (typeof searchTerms[0] === 'string' && (searchTerms[0] as string).indexOf('..') > 0)) {
const searchValues = (searchTerms.length === 2) ? searchTerms : (searchTerms[0] as string).split('..');
searchValue1 = `${Array.isArray(searchValues) ? searchValues[0] : ''}`;
searchValue2 = `${Array.isArray(searchValues) ? searchValues[1] : ''}`;
} else {
const parsedSearchValue = (Array.isArray(inputSearchTerms) && inputSearchTerms.length > 0) ? inputSearchTerms[0] : '';
searchValue1 = parsedSearchValue === undefined || parsedSearchValue === null ? '' : `${parsedSearchValue}`; // make sure it's a string
}

if (searchValue1 !== undefined && searchValue2 !== undefined) {
parsedSearchValues.push(searchValue1 as string, searchValue2 as string);
} else if (searchValue1 !== undefined) {
parsedSearchValues.push(searchValue1 as string);
}
return parsedSearchValues;
}

/** Execute the filter string test condition, returns a boolean */
function testStringCondition(operator: OperatorType | OperatorString, cellValue: string, searchValue: string, searchInputLastChar?: string): boolean {
if (operator === '*' || operator === OperatorType.endsWith) {
return cellValue.endsWith(searchValue);
} else if ((operator === '' && searchInputLastChar === '*') || operator === OperatorType.startsWith) {
return cellValue.startsWith(searchValue);
} else if (operator === '' || operator === OperatorType.contains) {
return (cellValue.indexOf(searchValue) > -1);
} else if (operator === '<>' || operator === OperatorType.notContains) {
return (cellValue.indexOf(searchValue) === -1);
}
return testFilterCondition(operator || '==', cellValue, searchValue);
}
Loading

0 comments on commit e44145d

Please sign in to comment.