From 68c9d525b363d9665f1aa9351bdb8e10932b364e Mon Sep 17 00:00:00 2001 From: Georgi Anastasov Date: Mon, 15 Jul 2024 09:03:33 +0300 Subject: [PATCH 1/3] fix(advance-filtering): handle error when columns are dynamically changed --- .../grid/grid-filtering-advanced.spec.ts | 85 ++++++++++++++++++- ...-toolbar-advanced-filtering.component.html | 2 +- ...id-toolbar-advanced-filtering.component.ts | 35 ++++++++ .../query-builder/query-builder.component.ts | 38 +++++---- .../src/lib/test-utils/grid-samples.spec.ts | 31 +++++++ 5 files changed, 170 insertions(+), 21 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-advanced.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-advanced.spec.ts index ca69837bb04..50792ec137c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-advanced.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-advanced.spec.ts @@ -15,13 +15,15 @@ import { IgxGridAdvancedFilteringComponent, IgxGridExternalAdvancedFilteringComponent, IgxGridAdvancedFilteringBindingComponent, - IgxGridAdvancedFilteringOverlaySettingsComponent + IgxGridAdvancedFilteringOverlaySettingsComponent, + IgxGridAdvancedFilteringDynamicColumnsComponent } from '../../test-utils/grid-samples.spec'; import { ControlsFunction } from '../../test-utils/controls-functions.spec'; import { FormattedValuesFilteringStrategy } from '../../data-operations/filtering-strategy'; import { IgxHierGridExternalAdvancedFilteringComponent } from '../../test-utils/hierarchical-grid-components.spec'; import { IgxHierarchicalGridComponent } from '../hierarchical-grid/public_api'; import { IFilteringEventArgs } from '../public_api'; +import { SampleTestData } from '../../test-utils/sample-test-data.spec'; const ADVANCED_FILTERING_OPERATOR_LINE_AND_CSS_CLASS = 'igx-filter-tree__line--and'; const ADVANCED_FILTERING_OPERATOR_LINE_OR_CSS_CLASS = 'igx-filter-tree__line--or'; @@ -38,7 +40,8 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { IgxGridAdvancedFilteringComponent, IgxGridExternalAdvancedFilteringComponent, IgxGridAdvancedFilteringBindingComponent, - IgxHierGridExternalAdvancedFilteringComponent + IgxHierGridExternalAdvancedFilteringComponent, + IgxGridAdvancedFilteringDynamicColumnsComponent ] }); })); @@ -2048,6 +2051,84 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { expect(rows.length).toEqual(1, 'Wrong filtered rows count'); })); + it('should handle advanced filtering correctly when grid columns and data are dynamically changed', fakeAsync(() => { + const fixture = TestBed.createComponent(IgxGridAdvancedFilteringDynamicColumnsComponent); + grid = fixture.componentInstance.grid; + fixture.detectChanges(); + + grid.height = '800px'; + fixture.detectChanges(); + tick(50); + + expect(grid.filteredData).toBeNull(); + expect(grid.rowList.length).toBe(8); + expect(GridFunctions.getCurrentCellFromGrid(grid, 0, 1).value).toBe('Ignite UI for JavaScript'); + expect(GridFunctions.getCurrentCellFromGrid(grid, 1, 1).value).toBe('NetAdvantage'); + + // Open Advanced Filtering dialog + GridFunctions.clickAdvancedFilteringButton(fixture); + fixture.detectChanges(); + + // Click the initial 'Add And Group' button + GridFunctions.clickAdvancedFilteringInitialAddGroupButton(fixture, 0); + tick(100); + fixture.detectChanges(); + + // Populate edit inputs + selectColumnInEditModeExpression(fixture, 1); + selectOperatorInEditModeExpression(fixture, 2); + const input = GridFunctions.getAdvancedFilteringValueInput(fixture).querySelector('input'); + UIInteractions.clickAndSendInputElementValue(input, 'ign', fixture); + + // Commit the populated expression + GridFunctions.clickAdvancedFilteringExpressionCommitButton(fixture); + fixture.detectChanges(); + + // Apply the filters + GridFunctions.clickAdvancedFilteringApplyButton(fixture); + fixture.detectChanges(); + + // Verify the filter results + expect(grid.filteredData.length).toEqual(2); + expect(grid.rowList.length).toBe(2); + expect(GridFunctions.getCurrentCellFromGrid(grid, 0, 1).value).toBe('Ignite UI for JavaScript'); + expect(GridFunctions.getCurrentCellFromGrid(grid, 1, 1).value).toBe('Ignite UI for Angular'); + + // Change the grid's columns collection + fixture.componentInstance.columns = [ + { field: 'ID', header: 'ID', width: '200px', type: 'string' }, + { field: 'CompanyName', header: 'Company Name', width: '200px', type: 'string'}, + { field: 'ContactName', header: 'Contact Name', width: '200px', type: 'string' }, + { field: 'ContactTitle', header: 'Contact Title', width: '200px', type: 'string' }, + { field: 'City', header: 'City', width: '200px', type: 'string' }, + { field: 'Country', header: 'Country', width: '200px', type: 'string' }, + ]; + fixture.detectChanges(); + flush(); + + // Change the grid's data collection + setTimeout(() => { + grid.data = SampleTestData.contactInfoDataFull(); + fixture.detectChanges(); + flush(); + }); + + // Spy for error messages in the console + const consoleSpy = spyOn(console, 'error'); + + // Open Advanced Filtering dialog + GridFunctions.clickAdvancedFilteringButton(fixture); + fixture.detectChanges(); + flush(); + + // Verify the filters are cleared + expect(grid.filteredData).toEqual([]); + expect(grid.rowList.length).toBe(0); + + // Check for error messages in the console + expect(consoleSpy).not.toHaveBeenCalled(); + })); + describe('Context Menu - ', () => { it('Should discard added group when clicking its operator line without having a single expression.', fakeAsync(() => { // Open Advanced Filtering dialog. diff --git a/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.html b/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.html index 4c232bb7303..b4fca4d9ace 100644 --- a/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.html +++ b/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.html @@ -7,5 +7,5 @@ {{ grid?.resourceStrings.igx_grid_toolbar_advanced_filtering_button_label }} - ({{ numberOfColumns }}) + ({{ numberOfColumns }}) diff --git a/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts b/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts index ea36978b78f..da54d884cb7 100644 --- a/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts +++ b/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts @@ -45,6 +45,30 @@ export class IgxGridToolbarAdvancedFilteringComponent implements AfterViewInit { this.grid?.advancedFilteringExpressionsTreeChange.subscribe(filteringTree => { this.numberOfColumns = this.extractUniqueFieldNamesFromFilterTree(filteringTree).length; }); + this.grid?.filteringExpressionsTreeChange.subscribe(() => { + setTimeout(() => { + if (this.grid.advancedFilteringExpressionsTree) { + const dataKeys = Object.keys(this.grid.data[0]); + const filteringOperands = this.grid.advancedFilteringExpressionsTree.filteringOperands; + + const allOperandsPresent = filteringOperands.every(operand => { + if (operand instanceof FilteringExpressionsTree) { + // Recursively check nested filtering trees + return this.checkFilteringOperands(operand, dataKeys); + } else { + return dataKeys.includes(operand.fieldName); + } + }); + + if (!allOperandsPresent) { + setTimeout(() => { + this.grid.advancedFilteringExpressionsTree = this.grid.filteringExpressionsTree; + this.numberOfColumns = 0; + }); + } + } + }); + }); } /** @@ -66,4 +90,15 @@ export class IgxGridToolbarAdvancedFilteringComponent implements AfterViewInit { }); return [...new Set(columnNames)]; } + + private checkFilteringOperands(filteringTree: FilteringExpressionsTree, dataKeys: string[]): boolean { + return filteringTree.filteringOperands.every(operand => { + if (operand instanceof FilteringExpressionsTree) { + // Recursively check nested filtering trees + return this.checkFilteringOperands(operand, dataKeys); + } else { + return dataKeys.includes(operand.fieldName); + } + }); + } } diff --git a/projects/igniteui-angular/src/lib/query-builder/query-builder.component.ts b/projects/igniteui-angular/src/lib/query-builder/query-builder.component.ts index efbff961c69..0453a728d01 100644 --- a/projects/igniteui-angular/src/lib/query-builder/query-builder.component.ts +++ b/projects/igniteui-angular/src/lib/query-builder/query-builder.component.ts @@ -988,31 +988,33 @@ export class IgxQueryBuilderComponent implements AfterViewInit, OnDestroy { this.currentGroup = groupItem; } - private createExpressionGroupItem(expressionTree: IExpressionTree, parent?: ExpressionGroupItem): ExpressionGroupItem { - let groupItem: ExpressionGroupItem; - if (expressionTree) { - groupItem = new ExpressionGroupItem(expressionTree.operator, parent); - - for (const expr of expressionTree.filteringOperands) { - if (expr instanceof FilteringExpressionsTree) { - groupItem.children.push(this.createExpressionGroupItem(expr, groupItem)); - } else { - const filteringExpr = expr as IFilteringExpression; - const exprCopy: IFilteringExpression = { - fieldName: filteringExpr.fieldName, - condition: filteringExpr.condition, - searchVal: filteringExpr.searchVal, - ignoreCase: filteringExpr.ignoreCase - }; + private createExpressionGroupItem(expressionTree: IExpressionTree, parent?: ExpressionGroupItem): ExpressionGroupItem | null { + if (!expressionTree) { + return null; + } + + const groupItem = new ExpressionGroupItem(expressionTree.operator, parent); + + for (const expr of expressionTree.filteringOperands) { + if (expr instanceof FilteringExpressionsTree) { + const childGroup = this.createExpressionGroupItem(expr, groupItem); + if (childGroup) { + groupItem.children.push(childGroup); + } + } else { + const filteringExpr = expr as IFilteringExpression; + const field = this.fields.find(el => el.field === filteringExpr.fieldName); + + if (field) { + const exprCopy: IFilteringExpression = { ...filteringExpr }; const operandItem = new ExpressionOperandItem(exprCopy, groupItem); - const field = this.fields.find(el => el.field === filteringExpr.fieldName); operandItem.fieldLabel = field.label || field.header || field.field; groupItem.children.push(operandItem); } } } - return groupItem; + return groupItem.children.length > 0 ? groupItem : null; } private createExpressionTreeFromGroupItem(groupItem: ExpressionGroupItem): FilteringExpressionsTree { diff --git a/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts b/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts index 32d20e821eb..f435bd2df80 100644 --- a/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts @@ -1174,6 +1174,37 @@ export class IgxGridAdvancedFilteringComponent extends BasicGridComponent { } } +@Component({ + template: ` + + + + `, + standalone: true, + imports: [NgFor, IgxGridComponent, IgxColumnComponent, IgxGridToolbarComponent] +}) +export class IgxGridAdvancedFilteringDynamicColumnsComponent extends BasicGridComponent { + public override data = []; + public columns = []; + + public ngOnInit(): void { + this.columns = [ + { field: 'ID', header: 'HeaderID', width: '100px', type: 'number' }, + { field: 'ProductName', header: 'Product Name', width: '200px', type: 'string'}, + { field: 'Downloads', header: 'Downloads', width: '100px', type: 'number' }, + { field: 'Released', header: 'Released', width: '100px', type: 'boolean' }, + { field: 'ReleaseDate', header: 'Release Date', width: '200px', type: 'date' }, + { field: 'AnotherField', header: 'Another Field', width: '200px', type: 'string' }, + ]; + this.data = SampleTestData.excelFilteringData(); + } +} + @Component({ template: ` From b5e6678ed8cc284040032a605317758e99667348 Mon Sep 17 00:00:00 2001 From: Georgi Anastasov Date: Fri, 19 Jul 2024 15:19:34 +0300 Subject: [PATCH 2/3] fix(advance-filtering): remove unnecessary setTimeout functions --- ...id-toolbar-advanced-filtering.component.ts | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts b/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts index 0b9a2e74631..7a238971ede 100644 --- a/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts +++ b/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts @@ -51,28 +51,24 @@ export class IgxGridToolbarAdvancedFilteringComponent implements AfterViewInit { this.numberOfColumns = this.extractUniqueFieldNamesFromFilterTree(filteringTree).length; }); this.grid?.filteringExpressionsTreeChange.subscribe(() => { - setTimeout(() => { - if (this.grid.advancedFilteringExpressionsTree) { - const dataKeys = Object.keys(this.grid.data[0]); - const filteringOperands = this.grid.advancedFilteringExpressionsTree.filteringOperands; + if (this.grid.advancedFilteringExpressionsTree) { + const dataKeys = Object.keys(this.grid.data[0]); + const filteringOperands = this.grid.advancedFilteringExpressionsTree.filteringOperands; - const allOperandsPresent = filteringOperands.every(operand => { - if (operand instanceof FilteringExpressionsTree) { - // Recursively check nested filtering trees - return this.checkFilteringOperands(operand, dataKeys); - } else { - return dataKeys.includes(operand.fieldName); - } - }); - - if (!allOperandsPresent) { - setTimeout(() => { - this.grid.advancedFilteringExpressionsTree = this.grid.filteringExpressionsTree; - this.numberOfColumns = 0; - }); + const allOperandsPresent = filteringOperands.every(operand => { + if (operand instanceof FilteringExpressionsTree) { + // Recursively check nested filtering trees + return this.checkFilteringOperands(operand, dataKeys); + } else { + return dataKeys.includes(operand.fieldName); } + }); + + if (!allOperandsPresent) { + this.grid.advancedFilteringExpressionsTree = this.grid.filteringExpressionsTree; + this.numberOfColumns = 0; } - }); + } }); } From a655184f4b1b6f3af77f84426dd0a3dbb586cdc3 Mon Sep 17 00:00:00 2001 From: Georgi Anastasov Date: Mon, 22 Jul 2024 16:12:16 +0300 Subject: [PATCH 3/3] chore(advance-filtering): restore initial state --- ...-toolbar-advanced-filtering.component.html | 4 +-- ...id-toolbar-advanced-filtering.component.ts | 31 ------------------- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.html b/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.html index 76dd216157e..57869977324 100644 --- a/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.html +++ b/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.html @@ -8,6 +8,6 @@ - {{ grid?.resourceStrings.igx_grid_toolbar_advanced_filtering_button_label }} - ({{ numberOfColumns }}) + {{ grid?.resourceStrings.igx_grid_toolbar_advanced_filtering_button_label }} + ({{ numberOfColumns }}) diff --git a/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts b/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts index 7a238971ede..415a83ffb56 100644 --- a/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts +++ b/projects/igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component.ts @@ -50,26 +50,6 @@ export class IgxGridToolbarAdvancedFilteringComponent implements AfterViewInit { this.grid?.advancedFilteringExpressionsTreeChange.subscribe(filteringTree => { this.numberOfColumns = this.extractUniqueFieldNamesFromFilterTree(filteringTree).length; }); - this.grid?.filteringExpressionsTreeChange.subscribe(() => { - if (this.grid.advancedFilteringExpressionsTree) { - const dataKeys = Object.keys(this.grid.data[0]); - const filteringOperands = this.grid.advancedFilteringExpressionsTree.filteringOperands; - - const allOperandsPresent = filteringOperands.every(operand => { - if (operand instanceof FilteringExpressionsTree) { - // Recursively check nested filtering trees - return this.checkFilteringOperands(operand, dataKeys); - } else { - return dataKeys.includes(operand.fieldName); - } - }); - - if (!allOperandsPresent) { - this.grid.advancedFilteringExpressionsTree = this.grid.filteringExpressionsTree; - this.numberOfColumns = 0; - } - } - }); } /** @@ -91,15 +71,4 @@ export class IgxGridToolbarAdvancedFilteringComponent implements AfterViewInit { }); return [...new Set(columnNames)]; } - - private checkFilteringOperands(filteringTree: FilteringExpressionsTree, dataKeys: string[]): boolean { - return filteringTree.filteringOperands.every(operand => { - if (operand instanceof FilteringExpressionsTree) { - // Recursively check nested filtering trees - return this.checkFilteringOperands(operand, dataKeys); - } else { - return dataKeys.includes(operand.fieldName); - } - }); - } }