Skip to content

Commit

Permalink
feat(collection): support multiple filterBy/sortBy in CollectionServi…
Browse files Browse the repository at this point in the history
…ce (#106)

* feat(collection): support multiple filterBy/sortBy in CollectionService
- this allows to pass an array or a simple object to both `collectionFilterBy` and `collectionSortBy`

* refactor(pass): filterResultAfterEachPass w/options "merged", "chained
- also changed operatory type that are acceptable, IN & NOT_IN are no longer allowed types

* refactor(option): use option at present time (merge/chain)

* refactor(build): fix build missing types

* refactor(build): fix wrong indentation

* refactor(comments): add comments and change variable name
  • Loading branch information
ghiscoding committed Oct 5, 2018
1 parent d9ccdf2 commit 86e8561
Show file tree
Hide file tree
Showing 18 changed files with 350 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,8 @@ export class SelectEditor implements Editor {
// user might want to filter certain items of the collection
if (this.columnEditor && this.columnEditor.collectionFilterBy) {
const filterBy = this.columnEditor.collectionFilterBy;
outputCollection = this.collectionService.filterCollection(outputCollection, filterBy);
const filterCollectionBy = this.columnEditor.collectionOptions && this.columnEditor.collectionOptions.filterAfterEachPass || null;
outputCollection = this.collectionService.filterCollection(outputCollection, filterBy, filterCollectionBy);
}

return outputCollection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ export class SelectFilter implements Filter {
// user might want to filter certain items of the collection
if (this.columnFilter && this.columnFilter.collectionFilterBy) {
const filterBy = this.columnFilter.collectionFilterBy;
outputCollection = this.collectionService.filterCollection(outputCollection, filterBy);
const filterCollectionBy = this.columnFilter.collectionOptions && this.columnFilter.collectionOptions.filterResultAfterEachPass || null;
outputCollection = this.collectionService.filterCollection(outputCollection, filterBy, filterCollectionBy);
}

return outputCollection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { OperatorType } from './operatorType.enum';
export interface CollectionFilterBy {
property: string;
value: any;
operator?: OperatorType.equal | OperatorType.notEqual | OperatorType.in | OperatorType.notIn | OperatorType.contains
operator?: OperatorType.equal | OperatorType.notEqual | OperatorType.contains | OperatorType.notContains;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { FilterMultiplePassType } from './filterMultiplePassType.enum';
import { FilterMultiplePassTypeString } from './filterMultiplePassTypeString';

export interface CollectionOption {
/**
* Optionally add a blank entry to the beginning of the collection.
Expand All @@ -15,6 +18,14 @@ export interface CollectionOption {
*/
collectionInObjectProperty?: string;

/**
* Defaults to "chain", when using multiple "collectionFilterBy", do we want to "merge" or "chain" the result after each pass?
* For example if we have 2 filters to pass by, and we start with pass 1 returning 7 items and last pass returning 5 items
* "chain" is the default and will return 5 items, since the result of each pass is sent used by the next pass
* "merge" would return the merge of the 7 items & 5 items (without duplicates), since some item might be the same the result is anywhere between 5 to 13 items
*/
filterResultAfterEachPass?: FilterMultiplePassType | FilterMultiplePassTypeString;

/** defaults to empty, when using label with prefix/suffix, do we want to add a separator between each text (like a white space) */
separatorBetweenTextLabels?: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export interface ColumnEditor {
/** Options to change the behavior of the "collection" */
collectionOptions?: CollectionOption;

/** We could filter some items from the collection */
collectionFilterBy?: CollectionFilterBy;
/** We could filter some 1 or more items from the collection */
collectionFilterBy?: CollectionFilterBy | CollectionFilterBy[];

/** We could sort the collection by their value, or by translated value when enableTranslateLabel is True */
collectionSortBy?: CollectionSortBy;
/** We could sort the collection by 1 or more properties, or by translated value(s) when enableTranslateLabel is True */
collectionSortBy?: CollectionSortBy | CollectionSortBy[];

/** A custom structure can be used instead of the default label/value pair. Commonly used with Select/Multi-Select Editor */
customStructure?: CollectionCustomStructure;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ export interface ColumnFilter {
/** A collection of items/options that will be loaded asynchronously (commonly used with a Select/Multi-Select Editor) */
collectionAsync?: Promise<any>;

/** We could filter 1 or more items from the collection */
collectionFilterBy?: CollectionFilterBy | CollectionFilterBy[];

/** Options to change the behavior of the "collection" */
collectionOptions?: CollectionOption;

/** We could filter some items from the collection */
collectionFilterBy?: CollectionFilterBy;

/** We could sort the collection by their value, or by translated value when enableTranslateLabel is True */
collectionSortBy?: CollectionSortBy;
/** We could sort the collection by 1 or more properties, or by translated value(s) when enableTranslateLabel is True */
collectionSortBy?: CollectionSortBy | CollectionSortBy[];

/** A custom structure can be used instead of the default label/value pair. Commonly used with Select/Multi-Select Filter */
customStructure?: CollectionCustomStructure;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum FilterMultiplePassType {
merge = 'merge',
chain = 'chain'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type FilterMultiplePassTypeString = 'merge' | 'chain';
2 changes: 2 additions & 0 deletions aurelia-slickgrid/src/aurelia-slickgrid/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export * from './filterCallback.interface';
export * from './filterChangedArgs.interface';
export * from './filterCondition.interface';
export * from './filterConditionOption.interface';
export * from './filterMultiplePassType.enum';
export * from './filterMultiplePassTypeString';
export * from './formatter.interface';
export * from './graphqlDatasetFilter.interface';
export * from './graphqlCursorPaginationOption.interface';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export enum OperatorType {
/** value contains x */
contains = 'Contains',

/** value not contains x (inversed of contains) */
notContains = 'Not_Contains',

/** value less than x */
lessThan = 'LT',

Expand Down
101 changes: 79 additions & 22 deletions aurelia-slickgrid/src/aurelia-slickgrid/services/collection.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,53 @@ import { I18N } from 'aurelia-i18n';
import {
CollectionFilterBy,
CollectionSortBy,
FilterMultiplePassType,
FilterMultiplePassTypeString,
FieldType,
OperatorType,
SortDirectionNumber,
} from './../models/index';
import { sortByFieldType } from '../sorters/sorterUtilities';
import { uniqueArray } from './utilities';

@inject(I18N)
export class CollectionService {
constructor(private i18n: I18N) { }

/**
* Filter items from a collection
* Filter 1 or more items from a collection
* @param collection
* @param filterByOptions
*/
filterCollection(collection: any[], filterByOptions: CollectionFilterBy | CollectionFilterBy[], filterResultBy: FilterMultiplePassType | FilterMultiplePassTypeString | null = FilterMultiplePassType.chain): any[] {
let filteredCollection: any[] = [];

// when it's array, we will use the new filtered collection after every pass
// basically if input collection has 10 items on 1st pass and 1 item is filtered out, then on 2nd pass the input collection will be 9 items
if (Array.isArray(filterByOptions)) {
filteredCollection = (filterResultBy === FilterMultiplePassType.merge) ? [] : [...collection];

for (const filter of filterByOptions) {
if (filterResultBy === FilterMultiplePassType.merge) {
const filteredPass = this.singleFilterCollection(collection, filter);
filteredCollection = uniqueArray([...filteredCollection, ...filteredPass]);
} else {
filteredCollection = this.singleFilterCollection(filteredCollection, filter);
}
}
} else {
filteredCollection = this.singleFilterCollection(collection, filterByOptions);
}

return filteredCollection;
}

/**
* Filter an item from a collection
* @param collection
* @param filterBy
*/
filterCollection(collection: any[], filterBy: CollectionFilterBy): any[] {
singleFilterCollection(collection: any[], filterBy: CollectionFilterBy): any[] {
let filteredCollection: any[] = [];

if (filterBy) {
Expand All @@ -30,15 +62,13 @@ export class CollectionService {
case OperatorType.equal:
filteredCollection = collection.filter((item) => item[property] === value);
break;
case OperatorType.in:
filteredCollection = collection.filter((item) => item[property].indexOf(value) !== -1);
break;
case OperatorType.notIn:
filteredCollection = collection.filter((item) => item[property].indexOf(value) === -1);
break;
case OperatorType.contains:
filteredCollection = collection.filter((item) => value.indexOf(item[property]) !== -1);
filteredCollection = collection.filter((item) => item[property].toString().indexOf(value.toString()) !== -1);
break;
case OperatorType.notContains:
filteredCollection = collection.filter((item) => item[property].toString().indexOf(value.toString()) === -1);
break;
case OperatorType.notEqual:
default:
filteredCollection = collection.filter((item) => item[property] !== value);
}
Expand All @@ -48,25 +78,52 @@ export class CollectionService {
}

/**
* Sort items in a collection
* Sort 1 or more items in a collection
* @param collection
* @param sortBy
* @param sortByOptions
* @param enableTranslateLabel
*/
sortCollection(collection: any[], sortBy: CollectionSortBy, enableTranslateLabel?: boolean): any[] {
sortCollection(collection: any[], sortByOptions: CollectionSortBy | CollectionSortBy[], enableTranslateLabel?: boolean): any[] {
let sortedCollection: any[] = [];

if (sortBy) {
const property = sortBy.property || '';
const sortDirection = sortBy.hasOwnProperty('sortDesc') ? (sortBy.sortDesc ? -1 : 1) : 1;
const fieldType = sortBy.fieldType || FieldType.string;
if (sortByOptions) {
if (Array.isArray(sortByOptions)) {
// multi-sort
sortedCollection = collection.sort((dataRow1: any, dataRow2: any) => {
for (let i = 0, l = sortByOptions.length; i < l; i++) {
const sortBy = sortByOptions[i];

if (sortBy) {
const sortDirection = sortBy.sortDesc ? SortDirectionNumber.desc : SortDirectionNumber.asc;
const propertyName = sortBy.property || '';
const fieldType = sortBy.fieldType || FieldType.string;
const value1 = (enableTranslateLabel) ? this.i18n.tr(dataRow1[propertyName] || ' ') : dataRow1[propertyName];
const value2 = (enableTranslateLabel) ? this.i18n.tr(dataRow2[propertyName] || ' ') : dataRow2[propertyName];

sortedCollection = collection.sort((dataRow1: any, dataRow2: any) => {
const value1 = (enableTranslateLabel) ? this.i18n.tr(dataRow1[property] || ' ') : dataRow1[property];
const value2 = (enableTranslateLabel) ? this.i18n.tr(dataRow2[property] || ' ') : dataRow2[property];
const result = sortByFieldType(value1, value2, fieldType, sortDirection);
return result;
});
const sortResult = sortByFieldType(value1, value2, fieldType, sortDirection);
if (sortResult !== SortDirectionNumber.neutral) {
return sortResult;
}
}
}
return SortDirectionNumber.neutral;
});
} else {
// single sort
const propertyName = sortByOptions.property || '';
const sortDirection = sortByOptions.sortDesc ? SortDirectionNumber.desc : SortDirectionNumber.asc;
const fieldType = sortByOptions.fieldType || FieldType.string;

sortedCollection = collection.sort((dataRow1: any, dataRow2: any) => {
const value1 = (enableTranslateLabel) ? this.i18n.tr(dataRow1[propertyName] || ' ') : dataRow1[propertyName];
const value2 = (enableTranslateLabel) ? this.i18n.tr(dataRow2[propertyName] || ' ') : dataRow2[propertyName];
const sortResult = sortByFieldType(value1, value2, fieldType, sortDirection);
if (sortResult !== SortDirectionNumber.neutral) {
return sortResult;
}
return SortDirectionNumber.neutral;
});
}
}

return sortedCollection;
Expand Down
11 changes: 11 additions & 0 deletions aurelia-slickgrid/src/aurelia-slickgrid/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,3 +432,14 @@ export function getScrollBarWidth() {
$outer.remove();
return Math.ceil(100 - widthWithScroll);
}

/**
* Takes an input array and makes sure the array has unique values by removing duplicates
* @param array input with possible duplicates
* @return array output without duplicates
*/
export function uniqueArray(arr: any[]): any[] {
return arr.filter((item: any, index: number) => {
return arr.indexOf(item) >= index;
});
}
16 changes: 11 additions & 5 deletions aurelia-slickgrid/src/examples/slickgrid/example4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,17 @@ export class Example4 {
// OR 2- use "aurelia-fetch-client", they are both supported
collectionAsync: this.httpFetch.fetch(URL_SAMPLE_COLLECTION_DATA),

// remove certain value(s) from the select dropdown
collectionFilterBy: {
// collectionFilterBy & collectionSortBy accept a single or multiple options
// we can exclude certains values 365 & 360 from the dropdown filter
collectionFilterBy: [{
property: 'value',
operator: OperatorType.notEqual,
value: 360
}, {
property: 'value',
operator: OperatorType.notEqual,
value: 365
},
}],

// sort the select dropdown in a descending order
collectionSortBy: {
Expand All @@ -111,7 +116,8 @@ export class Example4 {
labelSuffix: 'text',
},
collectionOptions: {
separatorBetweenTextLabels: ''
separatorBetweenTextLabels: ' ',
filterResultAfterEachPass: 'chain' // options are "merge" or "chain" (defaults to "chain")
},
// we could add certain option(s) to the "multiple-select" plugin
filterOptions: {
Expand Down Expand Up @@ -178,7 +184,7 @@ export class Example4 {
// use columnDef searchTerms OR use presets as shown below
presets: {
filters: [
{ columnId: 'duration', searchTerms: [2, 22, 44] },
{ columnId: 'duration', searchTerms: [10, 220] },
// { columnId: 'complete', searchTerms: ['5'], operator: '>' },
{ columnId: 'usDateShort', operator: '<', searchTerms: ['4/20/25'] },
// { columnId: 'effort-driven', searchTerms: [true] }
Expand Down
Loading

0 comments on commit 86e8561

Please sign in to comment.