Skip to content

Commit

Permalink
feat(tree): add Tree Data multi-column Filtering support
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding-SE committed Apr 9, 2020
1 parent e4fac11 commit f9b4863
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 99 deletions.
139 changes: 92 additions & 47 deletions packages/common/src/services/filter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class FilterService {
private _dataView: any;
private _grid: any;
private _onSearchChange: SlickEvent;
private _tmpPreFilteredData: number[];

constructor(private filterFactory: FilterFactory, private pubSubService: PubSubService, private sharedService: SharedService) {
this._onSearchChange = new Slick.Event();
Expand Down Expand Up @@ -168,8 +169,17 @@ export class FilterService {
dataView.setFilterArgs({ columnFilters: this._columnFilters, grid: this._grid, dataView });
dataView.setFilter(this.customLocalFilter.bind(this));

// bind any search filter change (e.g. input filter keyup event)
this._eventHandler.subscribe(this._onSearchChange, (e: KeyboardEvent, args: any) => {
// this.fixFilter(this.sharedService.hierarchicalDataset, args.searchTerms);
const isGridWithTreeData = this._gridOptions?.enableTreeData || false;

// When using Tree Data, we need to do it in 2 steps
// step 1. we need to prefilter (search) the data prior, the result will be an array of IDs which are the node(s) and their parent nodes when necessary.
// step 2. calling the DataView.refresh() is what triggers the final does the final filtering, with "customLocalFilter()" which will decide which rows should persist
if (isGridWithTreeData) {
this._tmpPreFilteredData = this.preFilterTreeData(this._dataView.getItems(), this._columnFilters);
}

const columnId = args.columnId;
if (columnId !== null) {
dataView.refresh();
Expand Down Expand Up @@ -249,7 +259,7 @@ export class FilterService {
}

/** Local Grid Filter search */
customLocalFilter(item: any, args: any) {
customLocalFilter(item: any, args: any): boolean {
const dataView = args?.dataView;
const grid = args?.grid;
const isGridWithTreeData = this._gridOptions?.enableTreeData || false;
Expand All @@ -262,62 +272,34 @@ export class FilterService {
treeDataOptions = this._columnWithTreeData.treeData;
const collapsedPropName = treeDataOptions?.collapsedPropName || '__collapsed';
const parentPropName = treeDataOptions?.parentPropName || '__parentId';
// console.log('item', console.log(this.sharedService.hierarchicalDataset))
if (item.__treeLevel === 0 && item.__hasChildren) {
// this.filterChildItem(this.sharedService.hierarchicalDataset, treeDataOptions, true);
}
const dataViewIdIdentifier = this._gridOptions?.datasetIdPropertyName ?? 'id';

if (item[parentPropName] !== null) {
let parent = dataView.getItemById(item[parentPropName]);
let parent = this._dataView.getItemById(item[parentPropName]);
while (parent) {
if (parent[collapsedPropName]) {
return false;
return false; // don't display any row that have their parent collapsed
}
parent = dataView.getItemById(parent[parentPropName]);
parent = this._dataView.getItemById(parent[parentPropName]);
}
}
}

if (typeof columnFilters === 'object') {
for (const columnId of Object.keys(columnFilters)) {
const columnFilter = columnFilters[columnId] as ColumnFilter;
const conditionOptions = this.getFilterConditionOptionsOrBoolean(item, columnFilter, columnId, grid, dataView);
if (typeof conditionOptions === 'boolean') {
return conditionOptions;
}

if (isGridWithTreeData && this._columnWithTreeData) {
const treeItemsPropName = treeDataOptions?.itemMapPropName || '__treeItems';
if (item[treeItemsPropName] === undefined) {
return false;
}
const defaultTreeColumnFilter = { columnId: this._columnWithTreeData.id, columnDef: this._columnWithTreeData, searchTerms: [''] } as ColumnFilter;
const treeColumnFilter = (columnFilters[this._columnWithTreeData.id] || defaultTreeColumnFilter) as ColumnFilter;
const treeConditionOptions = this.getFilterConditionOptionsOrBoolean(item, treeColumnFilter, this._columnWithTreeData.id, args.grid, dataView);
const foundInAnyTreeLevel = item[treeItemsPropName].find((childItem: string) => FilterConditions.executeMappedCondition({ ...treeConditionOptions as FilterConditionOption, cellValue: childItem }));
if (typeof treeConditionOptions === 'boolean') {
return treeConditionOptions;
// filter out any row items that aren't part of our pre-processed "filterMyFiles()" result
if (Array.isArray(this._tmpPreFilteredData)) {
return this._tmpPreFilteredData.includes(item[dataViewIdIdentifier]); // return true when found, false otherwise
}
} else {
if (typeof columnFilters === 'object') {
for (const columnId of Object.keys(columnFilters)) {
const columnFilter = columnFilters[columnId] as ColumnFilter;
const conditionOptions = this.getFilterConditionOptionsOrBoolean(item, columnFilter, columnId, grid, dataView);
if (typeof conditionOptions === 'boolean') {
return conditionOptions;
}

if (this._columnWithTreeData.id === columnId) {
// when the current column is the Tree Data column, we can loop through the treeItems mapping and execute the search on these values and we're done
if (!foundInAnyTreeLevel) {
return false;
}
} else {
// when the current column is NOT the Tree Data column
// 1. we need to skip any row that has children
// - we consider a row having children when its treeItems mapping has more than 1 item since every treeItems always include themselve in the array
// - for example if we are on 2nd row (Task 2) and __treeItems: ['Task 2'], then it has no children, however this would mean it has children __treeItems: ['Task 2', 'Task 3']
// 2. also run the condition of the row with current column filter
const hasChildren = Array.isArray(item[treeItemsPropName]) && item[treeItemsPropName].length > 1;
const hasFoundItem = FilterConditions.executeMappedCondition(conditionOptions as FilterConditionOption);
if ((!foundInAnyTreeLevel) || (!hasChildren && !hasFoundItem)) {
return false;
}
if (!FilterConditions.executeMappedCondition(conditionOptions as FilterConditionOption)) {
return false;
}
} else if (!FilterConditions.executeMappedCondition(conditionOptions as FilterConditionOption)) {
return false;
}
}
}
Expand Down Expand Up @@ -430,6 +412,69 @@ export class FilterService {
} as FilterConditionOption;
}

/**
* When using Tree Data, we need to prefilter (search) the data prior, the result will be an array of IDs which are the node(s) and their parent nodes when necessary.
* This will then be passed to the DataView setFilter(customLocalFilter), which will itself loop through the list of IDs and display/hide the row if found that array of IDs
* We do this in 2 steps so that we can still use the DataSet setFilter()
*/
preFilterTreeData(inputArray: any[], columnFilters: ColumnFilters) {
const treeDataOptions = this._columnWithTreeData?.treeData;
const parentPropName = treeDataOptions?.parentPropName || '__parentId';
const dataViewIdIdentifier = this._gridOptions?.datasetIdPropertyName ?? 'id';

const treeObj = {};
for (let i = 0; i < inputArray.length; i++) {
treeObj[inputArray[i][dataViewIdIdentifier]] = inputArray[i];
// as the filtered data is then used again as each subsequent letter
// we need to delete the .__used property, otherwise the logic below
// in the while loop (which checks for parents) doesn't work:
delete treeObj[inputArray[i][dataViewIdIdentifier]].__used;
}

const filteredChildrenAndParents = [];
for (let i = 0; i < inputArray.length; i++) {
const item = inputArray[i];
let matchFilter = true; // valid until it is proven to be invalid

// loop through all column filters and execute filter condition(s)
for (const columnId of Object.keys(columnFilters)) {
const columnFilter = columnFilters[columnId] as ColumnFilter;
const conditionOptionResult = this.getFilterConditionOptionsOrBoolean(item, columnFilter, columnId, this._grid, this._dataView);

if (item.hasOwnProperty(columnId)) {
const conditionResult = (typeof conditionOptionResult === 'boolean') ? conditionOptionResult : FilterConditions.executeMappedCondition(conditionOptionResult as FilterConditionOption);
if (conditionResult) {
// don't return true as need to check other keys in columnFilters
} else {
matchFilter = false;
continue;
}
} else {
matchFilter = false;
continue;
}
}

// build an array from the matched filters, anything valid from filter condition
// will be pushed to the filteredChildrenAndParents array
if (matchFilter) {
const len = filteredChildrenAndParents.length;
// add child (id):
filteredChildrenAndParents.splice(len, 0, item[dataViewIdIdentifier]);
let parent = treeObj[item[parentPropName]] || false;
while (parent) {
// only add parent (id) if not already added:
parent.__used || filteredChildrenAndParents.splice(len, 0, parent[dataViewIdIdentifier]);
// mark each parent as used to not use them again later:
treeObj[parent[dataViewIdIdentifier]].__used = true;
// try to find parent of the current parent, if exists:
parent = treeObj[parent[parentPropName]] || false;
}
}
}
return filteredChildrenAndParents;
}

getColumnFilters() {
return this._columnFilters;
}
Expand Down
41 changes: 0 additions & 41 deletions packages/common/src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,47 +171,6 @@ export function findItemInHierarchicalStructure(hierarchicalArray: any, predicat
return undefined;
}

/**
* Loop through the dataset and add all tree items data content (all the nodes under each branch) at the parent level including itself.
* This is to help in filtering the data afterward, we can simply filter the tree items array instead of having to through the tree on every filter.
* Portion of the code comes from this Stack Overflow answer https://stackoverflow.com/a/28094393/1212166
* For example if we have
* [
* { id: 1, title: 'Task 1'},
* { id: 2, title: 'Task 2', parentId: 1 },
* { id: 3, title: 'Task 3', parentId: 2 },
* { id: 4, title: 'Task 4', parentId: 2 }
* ]
* The array will be modified as follow (and if we filter/search for say "4", then we know the result will be row 1, 2, 4 because each treeItems contain "4")
* [
* { id: 1, title: 'Task 1', __treeItems: ['Task 1', 'Task 2', 'Task 3', 'Task 4']},
* { id: 2, title: 'Task 2', parentId: 1, __treeItems: ['Task 2', 'Task 3', 'Task 4'] },
* { id: 3, title: 'Task 3', parentId: 2, __treeItems: ['Task 3'] },
* { id: 4, title: 'Task 4', parentId: 2, __treeItems: ['Task 4'] }
* ]
*
* @param items
*/
export function modifyDatasetToAddTreeItemsMapping(items: any[], treeDataColumn: Column, dataView: any) {
const parentPropName = treeDataColumn.treeData?.parentPropName || '__parentId';
const treeItemsPropName = treeDataColumn.treeData?.itemMapPropName || '__treeItems';

for (let i = 0; i < items.length; i++) {
items[i][treeItemsPropName] = [items[i][treeDataColumn.id]];
let item = items[i];

if (item[parentPropName] !== null) {
let parent = dataView.getItemById(item[parentPropName]);

while (parent) {
parent[treeItemsPropName] = dedupePrimitiveArray(parent[treeItemsPropName].concat(item[treeItemsPropName]));
item = parent;
parent = dataView.getItemById(item[parentPropName]);
}
}
}
}

/**
* HTML encode using jQuery with a <div>
* Create a in-memory div, set it's inner text(which jQuery automatically encodes)
Expand Down
10 changes: 3 additions & 7 deletions packages/web-demo-vanilla-bundle/src/examples/example05.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import {
Filters,
Formatters,
GridOption,
modifyDatasetToAddTreeItemsMapping,
sortFlatArrayWithParentChildRef,
} from '@slickgrid-universal/common';
import { Slicker } from '@slickgrid-universal/vanilla-bundle';
import './example05.scss';
import { ExampleGridOptions } from './example-grid-options';
import './example05.scss';

const NB_ITEMS = 200;

Expand Down Expand Up @@ -38,8 +37,6 @@ export class Example5 {
this.dataset = this.mockDataset();
this.slickgridLwc.datasetHierarchical = convertParentChildFlatArrayToHierarchicalView($.extend(true, [], this.dataset), { parentPropName: 'parentId', childrenPropName: 'children' });
this.slickgridLwc.dataset = this.dataset;
modifyDatasetToAddTreeItemsMapping(this.dataset, this.columnDefinitions[0], this.dataViewObj);
// console.log(this.dataset);
}

dispose() {
Expand All @@ -51,7 +48,7 @@ export class Example5 {
{
id: 'title', name: 'Title', field: 'title', width: 220, cssClass: 'cell-title',
filterable: true, sortable: true,
queryFieldSorter: 'id', type: FieldType.number,
queryFieldSorter: 'id', type: FieldType.string,
formatter: Formatters.tree,
treeData: {
levelPropName: 'indent',
Expand All @@ -77,7 +74,7 @@ export class Example5 {
formatter: Formatters.dateIso,
},
{
id: 'effort-driven', name: 'Effort Driven', width: 80, minWidth: 20, maxWidth: 80, cssClass: 'cell-effort-driven', field: 'effortDriven',
id: 'effortDriven', name: 'Effort Driven', width: 80, minWidth: 20, maxWidth: 80, cssClass: 'cell-effort-driven', field: 'effortDriven',
formatter: Formatters.checkmarkMaterial, cannotTriggerInsert: true,
filterable: true,
filter: {
Expand Down Expand Up @@ -142,7 +139,6 @@ export class Example5 {
// update dataset and re-render (invalidate) the grid
this.slickgridLwc.dataset = resultSortedFlatDataset;
this.dataset = resultSortedFlatDataset;
modifyDatasetToAddTreeItemsMapping(this.dataset, this.columnDefinitions[0], this.dataViewObj);
this.gridObj.invalidate();

// scroll to the new row
Expand Down
6 changes: 2 additions & 4 deletions packages/web-demo-vanilla-bundle/src/examples/example06.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
Formatter,
Formatters,
OperatorType,
modifyDatasetToAddTreeItemsMapping,
sortHierarchicalArray,
} from '@slickgrid-universal/common';
import { Slicker } from '@slickgrid-universal/vanilla-bundle';
Expand Down Expand Up @@ -38,8 +37,6 @@ export class Example6 {
gridContainerElm.addEventListener('onslickergridcreated', this.handleOnSlickerGridCreated.bind(this));
this.slickgridLwc = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, null, this.datasetHierarchical);
this.dataViewObj = this.slickgridLwc.dataView;
// this.dataViewObj.setFilter(this.myFilter.bind(this));
modifyDatasetToAddTreeItemsMapping(this.dataViewObj.getItems(), this.columnDefinitions[0], this.dataViewObj);
}

dispose() {
Expand Down Expand Up @@ -237,9 +234,10 @@ export class Example6 {
{ id: 2, file: 'txt', files: [{ id: 3, file: 'todo.txt', dateModified: '2015-05-12T14:50:00.123Z', size: 0.7, }] },
{
id: 4, file: 'pdf', files: [
{ id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, },
{ id: 5, file: 'map.pdf', dateModified: '2015-05-21T10:22:00.123Z', size: 3.1, },
{ id: 6, file: 'internet-bill.pdf', dateModified: '2015-05-12T14:50:00.123Z', size: 1.4, },
{ id: 22, file: 'phone-bill.pdf', dateModified: '2015-05-01T07:50:00.123Z', size: 1.4, },
{ id: 23, file: 'phone-bill.pdf', dateModified: '2015-05-01T07:50:00.123Z', size: 1.4, },
]
},
{ id: 9, file: 'misc', files: [{ id: 10, file: 'something.txt', dateModified: '2015-02-26T16:50:00.123Z', size: 0.4, }] },
Expand Down

0 comments on commit f9b4863

Please sign in to comment.