Skip to content

Commit ff00688

Browse files
authored
feat(h-grid): add support for advanced filtering (#15562)
* feat(h-grid): Enhanced the advanced filtering dialog to support filtering in the nested levels.
1 parent 044db87 commit ff00688

22 files changed

+2761
-159
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ All notable changes for each version of this project will be documented in this
1313
- A column's `minWidth` and `maxWidth` constrain the user-specified `width` so that it cannot go outside their bounds.
1414
- In SSR mode grid with height 100% or with no height will render on the server with % size and with no data. The grid will show either the empty grid template or the loading indicator (if isLoading is true).
1515
- In SSR mode grid with width 100% or with no width will render on the server with % size and with all columns.
16+
- `IgxHierarchicalGrid`
17+
- Introduced a new advanced filtering capability that enables top-level records to be dynamically refined based on the attributes or data of their associated child records.
18+
- Added a new `schema` input property that can be used to pass collection of `EntityType` objects. This property is required for remote data scenarios.
19+
- `IgxQueryBuilderComponent`, `IgxAdvancedFilteringDialogComponent`
20+
- Added support for entities with hierarchical structure.
21+
- `EntityType`
22+
- A new optional property called `childEntities` has been introduced that can be used to create nested entities.
1623

1724
## 19.1.1
1825
### New Features
@@ -30,7 +37,7 @@ All notable changes for each version of this project will be documented in this
3037
- Introduced a new `expanded` input property, enabling dynamic control over the banner's state. The banner can now be programmatically set to expanded (visible) or collapsed (hidden) both initially and at runtime. Animations will trigger during runtime updates — the **open animation** plays when `expanded` is set to `true`, and the **close animation** plays when set to `false`. However, no animations will trigger when the property is set initially.
3138
- The banner's event lifecycle (`opening`, `opened`, `closing`, `closed`) only triggers through **user interactions** (e.g., clicking to open/close). Programmatic updates using the `expanded` property will not fire any events.
3239
- If the `expanded` property changes during an ongoing animation, the current animation will **stop** and the opposite animation will begin from the **point where the previous animation left off**. For instance, if the open animation (10 seconds) is interrupted at 6 seconds and `expanded` is set to `false`, the close animation (5 seconds) will start from its 3rd second.
33-
- `IgxQueryBuilder` has new design that comes with updated appearance and new functionality
40+
- `IgxQueryBuilder` has a new design that comes with an updated appearance and new functionality
3441
- `IgxQueryBuilderComponent`
3542
- Introduced the ability to create nested queries by specifying IN/NOT IN operators.
3643
- Introduced the ability to reposition condition chips by dragging or using `Arrow Up/Down`.

projects/igniteui-angular-elements/src/analyzer/elements.config.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ import {
77
} from "../../../igniteui-angular/src/public_api";
88
import { IgxPaginatorComponent } from "../../../igniteui-angular/src/lib/paginator/paginator.component";
99
import { IgxPaginatorToken } from "../../../igniteui-angular/src/lib/paginator/token";
10-
import { IgxActionStripComponent } from "../../../igniteui-angular/src/lib/action-strip/action-strip.component";
11-
import { IgxActionStripToken } from "../../../igniteui-angular/src/lib/action-strip/token";
12-
import { IgxGridEditingActionsComponent } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component";
13-
import { IgxGridActionsBaseDirective } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-actions-base.directive";
14-
import { IgxGridPinningActionsComponent } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-pinning-actions.component";
15-
import { IgxColumnComponent } from "../../../igniteui-angular/src/lib/grids/columns/column.component";
16-
import { IgxColumnGroupComponent } from "../../../igniteui-angular/src/lib/grids/columns/column-group.component";
17-
import { IgxColumnLayoutComponent } from "../../../igniteui-angular/src/lib/grids/columns/column-layout.component";
1810
import { IgxGridToolbarTitleComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/common";
1911
import { IgxGridToolbarActionsComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/common";
2012
import { IgxGridToolbarAdvancedFilteringComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component";
2113
import { IgxGridToolbarComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/grid-toolbar.component";
2214
import { IgxToolbarToken } from "../../../igniteui-angular/src/lib/grids/toolbar/token";
15+
import { IgxColumnComponent } from "../../../igniteui-angular/src/lib/grids/columns/column.component";
16+
import { IgxColumnGroupComponent } from "../../../igniteui-angular/src/lib/grids/columns/column-group.component";
2317
import { IgxRowIslandComponent } from "../../../igniteui-angular/src/lib/grids/hierarchical-grid/row-island.component";
18+
import { IgxActionStripComponent } from "../../../igniteui-angular/src/lib/action-strip/action-strip.component";
19+
import { IgxActionStripToken } from "../../../igniteui-angular/src/lib/action-strip/token";
20+
import { IgxGridEditingActionsComponent } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component";
21+
import { IgxGridActionsBaseDirective } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-actions-base.directive";
22+
import { IgxGridPinningActionsComponent } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-pinning-actions.component";
23+
import { IgxColumnLayoutComponent } from "../../../igniteui-angular/src/lib/grids/columns/column-layout.component";
2424
import { IgxGridToolbarExporterComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/grid-toolbar-exporter.component";
2525
import { IgxGridToolbarHidingComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/grid-toolbar-hiding.component";
2626
import { IgxGridToolbarPinningComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/grid-toolbar-pinning.component";

projects/igniteui-angular/src/lib/data-operations/expressions-tree-util.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,16 +178,17 @@ export function isTree(entry: IExpressionTree | IFilteringExpression): entry is
178178
* @param entities An array of entities to use for recreating the tree.
179179
* @returns The recreated expression tree.
180180
*/
181-
export function recreateTree(tree: IExpressionTree, entities: EntityType[]): IExpressionTree {
182-
const entity = entities.find(e => e.name === tree.entity);
181+
export function recreateTree(tree: IExpressionTree, entities: EntityType[], isRoot: boolean = false): IExpressionTree {
182+
const entity = isRoot ? entities[0] : entities.find(e => e.name === tree.entity);
183+
if (!entity) return tree;
183184

184185
for (let i = 0; i < tree.filteringOperands.length; i++) {
185186
const operand = tree.filteringOperands[i];
186187
if (isTree(operand)) {
187188
tree.filteringOperands[i] = recreateTree(operand, entities);
188189
} else {
189190
if (operand.searchTree) {
190-
operand.searchTree = recreateTree(operand.searchTree, entities);
191+
operand.searchTree = recreateTree(operand.searchTree, entities[0].childEntities ?? entities);
191192
}
192193
tree.filteringOperands[i] = recreateExpression(operand, entity?.fields);
193194
}

projects/igniteui-angular/src/lib/data-operations/filtering-strategy.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { FilteringLogic, IFilteringExpression } from './filtering-expression.interface';
22
import { FilteringExpressionsTree, IFilteringExpressionsTree } from './filtering-expressions-tree';
33
import { resolveNestedPath, parseDate, formatDate, formatCurrency } from '../core/utils';
4-
import { ColumnType, GridType } from '../grids/common/grid.interface';
4+
import { ColumnType, EntityType, GridType } from '../grids/common/grid.interface';
55
import { GridColumnDataType } from './data-util';
66
import { SortingDirection } from './sorting-strategy';
77
import { formatNumber, formatPercent, getLocaleCurrencyCode } from '@angular/common';
88
import { IFilteringState } from './filtering-state.interface';
99
import { isTree } from './expressions-tree-util';
10+
import { IgxHierarchicalGridComponent } from '../grids/hierarchical-grid/hierarchical-grid.component';
1011

1112
const DateType = 'date';
1213
const DateTimeType = 'dateTime';
@@ -39,14 +40,34 @@ export interface IgxFilterItem {
3940
export abstract class BaseFilteringStrategy implements IFilteringStrategy {
4041
// protected
4142
public findMatchByExpression(rec: any, expr: IFilteringExpression, isDate?: boolean, isTime?: boolean, grid?: GridType): boolean {
43+
if (expr.searchTree) {
44+
const records = rec[expr.searchTree.entity];
45+
const shouldMatchRecords = expr.conditionName === 'inQuery';
46+
if (!records) { // child grid is not yet created
47+
return true;
48+
} else if (records.length === 0) { // child grid is empty
49+
return false;
50+
}
51+
52+
for (let index = 0; index < records.length; index++) {
53+
const record = records[index];
54+
if ((shouldMatchRecords && this.matchRecord(record, expr.searchTree, grid, expr.searchTree.entity)) ||
55+
(!shouldMatchRecords && !this.matchRecord(record, expr.searchTree, grid, expr.searchTree.entity))) {
56+
return true;
57+
}
58+
}
59+
60+
return false;
61+
}
62+
4263
const val = this.getFieldValue(rec, expr.fieldName, isDate, isTime, grid);
4364
if (expr.condition?.logic) {
4465
return expr.condition.logic(val, expr.searchVal, expr.ignoreCase);
4566
}
4667
}
4768

4869
// protected
49-
public matchRecord(rec: any, expressions: IFilteringExpressionsTree | IFilteringExpression, grid?: GridType): boolean {
70+
public matchRecord(rec: any, expressions: IFilteringExpressionsTree | IFilteringExpression, grid?: GridType, entity?: string): boolean {
5071
if (expressions) {
5172
if (isTree(expressions)) {
5273
const expressionsTree = expressions;
@@ -55,7 +76,7 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy {
5576

5677
if (expressionsTree.filteringOperands && expressionsTree.filteringOperands.length) {
5778
for (const operand of expressionsTree.filteringOperands) {
58-
matchOperand = this.matchRecord(rec, operand, grid);
79+
matchOperand = this.matchRecord(rec, operand, grid, entity);
5980

6081
// Return false if at least one operand does not match and the filtering logic is And
6182
if (!matchOperand && operator === FilteringLogic.And) {
@@ -74,16 +95,42 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy {
7495
return true;
7596
} else {
7697
const expression = expressions;
77-
const column = grid && grid.getColumnByName(expression.fieldName);
78-
const isDate = column ? column.dataType === DateType || column.dataType === DateTimeType : false;
79-
const isTime = column ? column.dataType === TimeType : false;
98+
let dataType = null;
99+
if (!entity) {
100+
const column = grid && grid.getColumnByName(expression.fieldName);
101+
dataType = column?.dataType;
102+
} else if (grid.type === 'hierarchical') {
103+
const schema = (grid as IgxHierarchicalGridComponent).schema;
104+
const entityMatch = this.findEntityByName(schema, entity);
105+
dataType = entityMatch?.fields.find(f => f.field === expression.fieldName)?.dataType;
106+
}
107+
108+
const isDate = dataType ? dataType === DateType || dataType === DateTimeType : false;
109+
const isTime = dataType ? dataType === TimeType : false;
110+
80111
return this.findMatchByExpression(rec, expression, isDate, isTime, grid);
81112
}
82113
}
83114

84115
return true;
85116
}
86117

118+
private findEntityByName(schema: EntityType[], name: string): EntityType | null {
119+
for (const entity of schema) {
120+
if (entity.name === name) {
121+
return entity;
122+
}
123+
124+
if (entity.childEntities && entity.childEntities.length > 0) {
125+
const found = this.findEntityByName(entity.childEntities, name);
126+
if (found) {
127+
return found;
128+
}
129+
}
130+
}
131+
return null;
132+
}
133+
87134
public getFilterItems(column: ColumnType, tree: IFilteringExpressionsTree): Promise<IgxFilterItem[]> {
88135

89136
let data = column.grid.gridAPI.filterDataByExpressions(tree);

projects/igniteui-angular/src/lib/grids/common/grid.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,4 +1497,5 @@ export interface IClipboardOptions {
14971497
export interface EntityType {
14981498
name: string;
14991499
fields: FieldType[];
1500+
childEntities?: EntityType[];
15001501
}

projects/igniteui-angular/src/lib/grids/filtering/advanced-filtering/advanced-filtering-dialog.component.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { IDragStartEventArgs, IgxDragDirective, IgxDragHandleDirective } from '.
66
import { Subject } from 'rxjs';
77
import { IActiveNode } from '../../grid-navigation.service';
88
import { PlatformUtil } from '../../../core/utils';
9-
import { FieldType, GridType } from '../../common/grid.interface';
9+
import { EntityType, FieldType, GridType } from '../../common/grid.interface';
1010
import { IgxQueryBuilderComponent } from '../../../query-builder/query-builder.component';
1111
import { GridResourceStringsEN } from '../../../core/i18n/grid-resources';
1212
import { IFilteringExpressionsTree } from '../../../data-operations/filtering-expressions-tree';
@@ -15,6 +15,8 @@ import { IgxQueryBuilderHeaderComponent } from '../../../query-builder/query-bui
1515
import { NgClass } from '@angular/common';
1616
import { getCurrentResourceStrings } from '../../../core/i18n/resources';
1717
import { QueryBuilderResourceStringsEN } from '../../../core/i18n/query-builder-resources';
18+
import { IgxHierarchicalGridComponent } from '../../hierarchical-grid/hierarchical-grid.component';
19+
import { IgxRowIslandComponent } from '../../hierarchical-grid/row-island.component';
1820

1921
/**
2022
* A component used for presenting advanced filtering UI for a Grid.
@@ -191,18 +193,33 @@ export class IgxAdvancedFilteringDialogComponent implements AfterViewInit, OnDes
191193
this.closeDialog();
192194
}
193195

194-
195196
/**
196197
* @hidden @internal
197198
*/
198199
public generateEntity() {
199-
const entities = [
200-
{
201-
name: null,
202-
fields: this.filterableFields
203-
}
204-
];
205-
return entities;
200+
if (this.queryBuilder?.entities) {
201+
return this.queryBuilder?.entities;
202+
} else if (this.grid.type === 'hierarchical') {
203+
return (this.grid as IgxHierarchicalGridComponent).schema;
204+
} else {
205+
const entities: EntityType[] = [
206+
{
207+
name: null,
208+
fields: this.filterableFields.map(f => ({
209+
field: f.field,
210+
dataType: f.dataType,
211+
// label: f.label,
212+
// header: f.header,
213+
editorOptions: f.editorOptions,
214+
filters: f.filters,
215+
pipeArgs: f.pipeArgs,
216+
defaultTimeFormat: f.defaultTimeFormat,
217+
defaultDateTimeFormat: f.defaultDateTimeFormat
218+
})) as FieldType[]
219+
}
220+
];
221+
return entities;
222+
}
206223
}
207224

208225
private assignResourceStrings() {

projects/igniteui-angular/src/lib/grids/grid-base.directive.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ import {
147147
ISizeInfo,
148148
RowType,
149149
IPinningConfig,
150-
IClipboardOptions
150+
IClipboardOptions,
151+
EntityType
151152
} from './common/grid.interface';
152153
import { DropPosition } from './moving/moving.service';
153154
import { IgxHeadSelectorDirective, IgxRowSelectorDirective } from './selection/row-selectors';
@@ -179,7 +180,7 @@ import { DefaultDataCloneStrategy, IDataCloneStrategy } from '../data-operations
179180
import { IgxGridCellComponent } from './cell.component';
180181
import { IgxGridValidationService } from './grid/grid-validation.service';
181182
import { getCurrentResourceStrings } from '../core/i18n/resources';
182-
import { isTree, recreateTreeFromFields } from '../data-operations/expressions-tree-util';
183+
import { isTree, recreateTree, recreateTreeFromFields } from '../data-operations/expressions-tree-util';
183184
import { getUUID } from './common/random';
184185

185186
interface IMatchInfoCache {
@@ -1859,7 +1860,7 @@ export abstract class IgxGridBaseDirective implements GridType,
18591860

18601861
value.type = FilteringExpressionsTreeType.Regular;
18611862
if (value && this._columns?.length > 0) {
1862-
this._filteringExpressionsTree = recreateTreeFromFields(value, this._columns) as IFilteringExpressionsTree;
1863+
this._filteringExpressionsTree = this.getRecreatedTree(value);
18631864
} else {
18641865
this._filteringExpressionsTree = value;
18651866
}
@@ -1909,7 +1910,7 @@ export abstract class IgxGridBaseDirective implements GridType,
19091910
if (value && isTree(value)) {
19101911
value.type = FilteringExpressionsTreeType.Advanced;
19111912
if (this._columns && this._columns.length > 0) {
1912-
this._advancedFilteringExpressionsTree = recreateTreeFromFields(value, this._columns) as IFilteringExpressionsTree;
1913+
this._advancedFilteringExpressionsTree = this.getRecreatedTree(value);
19131914
} else {
19141915
this._advancedFilteringExpressionsTree = value;
19151916
}
@@ -3138,6 +3139,7 @@ export abstract class IgxGridBaseDirective implements GridType,
31383139
matchCount: 0,
31393140
content: ''
31403141
};
3142+
protected _hGridSchema: EntityType[];
31413143
protected gridComputedStyles;
31423144

31433145
/** @hidden @internal */
@@ -6638,10 +6640,10 @@ export abstract class IgxGridBaseDirective implements GridType,
66386640
this._unpinnedColumns = newColumns.filter((c) => !c.pinned);
66396641
this._columns = newColumns;
66406642
if (this._columns && this._columns.length && this._filteringExpressionsTree) {
6641-
this._filteringExpressionsTree = recreateTreeFromFields(this._filteringExpressionsTree, this.columns) as IFilteringExpressionsTree;
6643+
this._filteringExpressionsTree = this.getRecreatedTree(this._filteringExpressionsTree);
66426644
}
66436645
if (this._columns && this._columns.length && this._advancedFilteringExpressionsTree) {
6644-
this._advancedFilteringExpressionsTree = recreateTreeFromFields(this._advancedFilteringExpressionsTree, this.columns) as IFilteringExpressionsTree;
6646+
this._advancedFilteringExpressionsTree = this.getRecreatedTree(this._advancedFilteringExpressionsTree);
66456647
}
66466648
this.resetCaches();
66476649
}
@@ -6706,10 +6708,10 @@ export abstract class IgxGridBaseDirective implements GridType,
67066708
this._columns = this.getColumnList();
67076709
}
67086710
if (this._columns && this._columns.length && this._filteringExpressionsTree) {
6709-
this._filteringExpressionsTree = recreateTreeFromFields(this._filteringExpressionsTree, this._columns) as IFilteringExpressionsTree;
6711+
this._filteringExpressionsTree = this.getRecreatedTree(this._filteringExpressionsTree);
67106712
}
67116713
if (this._columns && this._columns.length && this._advancedFilteringExpressionsTree) {
6712-
this._advancedFilteringExpressionsTree = recreateTreeFromFields(this._advancedFilteringExpressionsTree, this._columns) as IFilteringExpressionsTree;
6714+
this._advancedFilteringExpressionsTree = this.getRecreatedTree(this._advancedFilteringExpressionsTree);
67136715
}
67146716

67156717
this.initColumns(this._columns, (col: IgxColumnComponent) => this.columnInit.emit(col));
@@ -7922,4 +7924,12 @@ export abstract class IgxGridBaseDirective implements GridType,
79227924
this.navigation.activeNode = {} as IActiveNode;
79237925
this.notifyChanges();
79247926
}
7927+
7928+
private getRecreatedTree(value: IFilteringExpressionsTree): IFilteringExpressionsTree {
7929+
if (this._hGridSchema) {
7930+
return recreateTree(value, this._hGridSchema, true) as IFilteringExpressionsTree;
7931+
} else {
7932+
return recreateTreeFromFields(value, this._columns) as IFilteringExpressionsTree;
7933+
}
7934+
}
79257935
}

0 commit comments

Comments
 (0)