From 28eec6be09e8af659f25c6cf4f37f604925d8710 Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Wed, 11 Jun 2025 20:08:31 +0300 Subject: [PATCH 1/7] feat(h-grid): update adv filtering schema on row expand --- .../grid/grid-filtering-advanced.spec.ts | 119 +++++++++--------- .../hierarchical-grid.component.ts | 69 ++++++---- 2 files changed, 103 insertions(+), 85 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 fce5caf5528..258a01f67f3 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 @@ -27,7 +27,7 @@ import { QueryBuilderFunctions } from '../../query-builder/query-builder-functio import { By } from '@angular/platform-browser'; import { IgxDateTimeEditorDirective } from '../../directives/date-time-editor/date-time-editor.directive'; import { QueryBuilderSelectors } from '../../query-builder/query-builder.common'; -import { IgxHGridRemoteOnDemandComponent } from '../hierarchical-grid/hierarchical-grid.spec'; +import { IgxHGridRemoteOnDemandComponent, IgxHierarchicalGridToggleRIComponent } from '../hierarchical-grid/hierarchical-grid.spec'; import { IGridResourceStrings } from '../../core/i18n/grid-resources'; describe('IgxGrid - Advanced Filtering #grid - ', () => { @@ -1750,76 +1750,69 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { expect(hgrid.filteredData.length).toBe(5); })); - it('Should have proper fields in UI when schema is defined with load on demand.', fakeAsync(() => { - const fixture = TestBed.createComponent(IgxHGridRemoteOnDemandComponent); - const hierarchicalGrid = fixture.componentInstance.instance; + it('Should update root grid schema when row island is expanded.', fakeAsync(() => { + const fixture = TestBed.createComponent(IgxHierarchicalGridToggleRIComponent); + const hierarchicalGrid = fixture.componentInstance.hgrid; hierarchicalGrid.allowAdvancedFiltering = true; - hierarchicalGrid.schema = [ - { - name: 'rootLevel', - fields: [ - { field: 'ID', dataType: 'string' }, - { field: 'ChildLevels', dataType: 'number' }, - { field: 'ProductName', dataType: 'string' }, - { field: 'Col1', dataType: 'number' }, - { field: 'Col2', dataType: 'number' }, - { field: 'Col3', dataType: 'number' } - ], - childEntities: [ - { - name: 'childData', - fields: [ - { field: 'ID', dataType: 'string' }, - { field: 'ProductName', dataType: 'string' } - ], - childEntities: [ - { - name: 'childData2', - fields: [ - { field: 'ID', dataType: 'string' }, - { field: 'ProductName', dataType: 'string' } - ] - } - ] - } - ] - } - ] - fixture.detectChanges(); - - hierarchicalGrid.openAdvancedFilteringDialog(); fixture.detectChanges(); - // Click the initial 'Add Condition' button. - QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fixture, 0); - tick(100); - fixture.detectChanges(); - // Populate edit inputs. - QueryBuilderFunctions.selectColumnInEditModeExpression(fixture, 0); // Select 'ID' column. - QueryBuilderFunctions.selectOperatorInEditModeExpression(fixture, 10); // Select 'In' operator. - tick(100); - fixture.detectChanges(); - - const entityInputGroup = QueryBuilderFunctions.getQueryBuilderEntitySelect(fixture, 1).querySelector('input'); - expect(entityInputGroup.value).toBe('childData'); + // Open advanced filtering dialog and create an 'In' condition. + const createInConditionAndGetReturnFields = () => { + // Open Advanced Filtering dialog. + hierarchicalGrid.openAdvancedFilteringDialog(); + fixture.detectChanges(); + + // Click the initial 'Add Condition' button. + QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fixture, 0); + tick(100); + fixture.detectChanges(); + // Populate edit inputs. + QueryBuilderFunctions.selectColumnInEditModeExpression(fixture, 0); // Select 'ID' column. + QueryBuilderFunctions.selectOperatorInEditModeExpression(fixture, 10); // Select 'In' operator. + tick(100); + fixture.detectChanges(); + + // Verify entities + QueryBuilderFunctions.clickQueryBuilderEntitySelect(fixture, 1); + fixture.detectChanges(); + const queryBuilderElement: HTMLElement = fixture.debugElement.queryAll(By.css(`.${QueryBuilderSelectors.QUERY_BUILDER_TREE}`))[1].nativeElement; + let dropdownValues: string[] = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement).map((x: any) => x.innerText); + let expectedValues = ['childData']; + expect(dropdownValues).toEqual(expectedValues); + + // Verify return fields + QueryBuilderFunctions.clickQueryBuilderFieldsCombo(fixture, 1); + fixture.detectChanges(); + const innerQueryReturnFields = QueryBuilderFunctions.getQueryBuilderSelectDropdown(queryBuilderElement, 1); + + return innerQueryReturnFields; + }; - const fieldInputGroup = QueryBuilderFunctions.getQueryBuilderFieldsCombo(fixture, 1).querySelector('input'); - expect(fieldInputGroup.value).toBe('ID'); + let innerQueryReturnFields = createInConditionAndGetReturnFields(); + expect(innerQueryReturnFields).toBeNull(); - // Verify entities - QueryBuilderFunctions.clickQueryBuilderEntitySelect(fixture, 1); + // Close Advanced Filtering dialog. + hierarchicalGrid.closeAdvancedFilteringDialog(false); + tick(200); fixture.detectChanges(); - const queryBuilderElement: HTMLElement = fixture.debugElement.queryAll(By.css(`.${QueryBuilderSelectors.QUERY_BUILDER_TREE}`))[1].nativeElement; - let dropdownValues: string[] = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement).map((x: any) => x.innerText); - let expectedValues = ['childData']; - expect(dropdownValues).toEqual(expectedValues); - // Verify return fileds - QueryBuilderFunctions.clickQueryBuilderFieldsCombo(fixture, 1); + // Expand row island + const row = hierarchicalGrid.gridAPI.get_row_by_index(0) as any; + UIInteractions.simulateClickAndSelectEvent(row.expander); fixture.detectChanges(); - dropdownValues = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement, 1).map((x: any) => x.innerText); - expectedValues = ['ID', 'ProductName']; - expect(dropdownValues).toEqual(expectedValues); + + // Open advanced filtering dialog and verify that schema is updated. + innerQueryReturnFields = createInConditionAndGetReturnFields(); + expect(innerQueryReturnFields).not.toBeNull(); + const items = Array.from(innerQueryReturnFields.querySelectorAll('.igx-drop-down__item')); + expect(items.length).toBe(7); + expect((items[0] as HTMLElement).innerText).toBe('ID'); + expect((items[1] as HTMLElement).innerText).toBe('ChildLevels'); + expect((items[2] as HTMLElement).innerText).toBe('ProductName'); + expect((items[3] as HTMLElement).innerText).toBe('Col1'); + expect((items[4] as HTMLElement).innerText).toBe('Col2'); + expect((items[5] as HTMLElement).innerText).toBe('Col3'); + expect((items[6] as HTMLElement).innerText).toBe('childData2'); })); it('Should correctly change resource strings for hierarchical Advanced Filtering dialog.', fakeAsync(() => { diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts index 69695e21ee2..2ad9af0cb09 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts @@ -587,11 +587,10 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti * const schema = this.grid.schema; * this.grid.schema = [{ name: 'Products', fields: [...], childEntities: [...] }]; * ``` + * @deprecated in version 20.1.0. */ @Input() - public set schema(entities: EntityType[]) { - this._hGridSchema = entities; - } + public set schema(entities: EntityType[]) {} /* blazorSuppress */ public get schema() { @@ -683,6 +682,10 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti this.batchEditing = val; }); } + this.columnsAutogenerated.subscribe((e) => { + this.updateRootGridSchema(e); + }); + super.ngOnInit(); } @@ -1256,7 +1259,7 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti ]; entities[0].childEntities = this.childLayoutList.reduce((acc, rowIsland) => { - return acc.concat(this.generateChildEntity(rowIsland, this.data[0][rowIsland.key][0])); + return acc.concat(this.generateChildEntity(rowIsland)); } , []); } @@ -1264,32 +1267,54 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti return entities; } - private generateChildEntity(rowIsland: IgxRowIslandComponent, firstRowData: any[]): EntityType { + private updateRootGridSchema(event: any) { + const schema = (this.rootGrid as IgxHierarchicalGridComponent).schema; + if (!schema || schema.length === 0) { + return; + } + + let path = []; + let parentIsland = this.parentIsland; + while (parentIsland) { + path.push(parentIsland.key); + parentIsland = parentIsland.parentIsland; + } + + path.reverse(); + if (path.length > 0) { + let childLevel = path[0]; + let childEntity; + while (childLevel) { + childEntity = schema[0].childEntities.find(e => e.name === childLevel); + childLevel = path.shift(); + } + + childEntity.fields = event.columns.filter((column) => !column.columnGroup && column.filterable) + .map(f => ({ + field: f.field, + dataType: f.dataType, + label: f.label, + header: f.header, + editorOptions: f.editorOptions, + filters: f.filters, + pipeArgs: f.pipeArgs, + defaultTimeFormat: f.defaultTimeFormat, + defaultDateTimeFormat: f.defaultDateTimeFormat + })) as FieldType[]; + } + } + + private generateChildEntity(rowIsland: IgxRowIslandComponent): EntityType { const entityName = rowIsland.key; let fields = []; let childEntities; - if (!rowIsland.autoGenerate) { + if (rowIsland.childColumns?.length > 0) { fields = flatten(rowIsland.childColumns.toArray()).filter(col => col.field) .map(f => ({ field: f.field, dataType: f.dataType })) as FieldType[]; - } else if (firstRowData) { - const rowIslandFields = Object.keys(firstRowData).map(key => { - if (firstRowData[key] instanceof Array) { - return null; - } - - return { - field: key, - dataType: this.resolveDataTypes(firstRowData[key]) - } - }); - fields = rowIslandFields.filter(f => f !== null) as FieldType[]; } const rowIslandChildEntities = rowIsland.childLayoutList.reduce((acc, childRowIsland) => { - if (!firstRowData) { - return null; - } - return acc.concat(this.generateChildEntity(childRowIsland, firstRowData[childRowIsland.key][0])); + return acc.concat(this.generateChildEntity(childRowIsland)); }, []); if (rowIslandChildEntities?.length > 0) { From 28f8a351792d2a4b0bb31c797f4cca6c0fe6b85d Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Fri, 4 Jul 2025 16:29:41 +0300 Subject: [PATCH 2/7] fix(adv-filtering): improve child entities fields population on row expand --- .../hierarchical-grid.component.ts | 40 +++++++++---------- .../query-builder-tree.component.ts | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts index 2ad9af0cb09..b44aff06c52 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts @@ -1237,7 +1237,7 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti } private generateSchema() { - const filterableFields = this.columns.filter((column) => !column.columnGroup && column.filterable); + const filterableFields: FieldType[] = this.columns.filter((column) => !column.columnGroup && column.filterable); let entities: EntityType[]; if(filterableFields.length !== 0) { @@ -1247,8 +1247,8 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti fields: filterableFields.map(f => ({ field: f.field, dataType: f.dataType, - // label: f.label, - // header: f.header, + label: f.label, + header: f.header, editorOptions: f.editorOptions, filters: f.filters, pipeArgs: f.pipeArgs, @@ -1282,25 +1282,25 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti path.reverse(); if (path.length > 0) { - let childLevel = path[0]; - let childEntity; - while (childLevel) { - childEntity = schema[0].childEntities.find(e => e.name === childLevel); - childLevel = path.shift(); + let childEntity = schema[0]; + for (let i = 0; i < path.length; i++) { + childEntity = childEntity.childEntities.find(e => e.name === path[i]); } - childEntity.fields = event.columns.filter((column) => !column.columnGroup && column.filterable) - .map(f => ({ - field: f.field, - dataType: f.dataType, - label: f.label, - header: f.header, - editorOptions: f.editorOptions, - filters: f.filters, - pipeArgs: f.pipeArgs, - defaultTimeFormat: f.defaultTimeFormat, - defaultDateTimeFormat: f.defaultDateTimeFormat - })) as FieldType[]; + if (childEntity) { + childEntity.fields = event.columns.filter((column) => !column.columnGroup && column.filterable) + .map(f => ({ + field: f.field, + dataType: f.dataType, + label: f.label, + header: f.header, + editorOptions: f.editorOptions, + filters: f.filters, + pipeArgs: f.pipeArgs, + defaultTimeFormat: f.defaultTimeFormat, + defaultDateTimeFormat: f.defaultDateTimeFormat + })) as FieldType[]; + } } } diff --git a/projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.ts b/projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.ts index 7a59165e80c..36f23c9ce98 100644 --- a/projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.ts +++ b/projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.ts @@ -1410,7 +1410,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { * @hidden @internal */ public getFormat(field: string) { - return this.fields?.find(el => el.field === field).pipeArgs.format; + return this.fields?.find(el => el.field === field)?.pipeArgs.format; } /** From d923eaaa24f80e4abad08d7caca15d7ca1d88924 Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Wed, 16 Jul 2025 14:32:38 +0300 Subject: [PATCH 3/7] chore(*): add deprecation of schema property to changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe5277e2a5..500f2267921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes for each version of this project will be documented in this file. +## 20.1.0 + +### General +- `IgxHierarchicalGrid` + - **Deprecation** - `schema` input property has been deprecated and will be removed in a future version. + ## 20.0.2 ### New Features From a418259c525a55843aaaf0864a8aef992c2748ee Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Tue, 7 Oct 2025 13:42:27 +0300 Subject: [PATCH 4/7] chore(*): update deprecation comment --- .../lib/grids/hierarchical-grid/hierarchical-grid.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts index 154fa3d581a..8a10cd7dfdd 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts @@ -590,7 +590,7 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti * const schema = this.grid.schema; * this.grid.schema = [{ name: 'Products', fields: [...], childEntities: [...] }]; * ``` - * @deprecated in version 20.1.0. + * @deprecated in version 21.0.0. */ @Input() public set schema(entities: EntityType[]) {} From fa96ebd7d90da6f5b438e91f299bd2ecf2737304 Mon Sep 17 00:00:00 2001 From: igdmdimitrov <49060557+igdmdimitrov@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:11:48 +0300 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: Galina Edinakova --- CHANGELOG.md | 2 +- .../lib/grids/hierarchical-grid/hierarchical-grid.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5217b1e22d..b39b7e1ac66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes for each version of this project will be documented in this file. -## 21.0.0 +## 20.2.0 ### General - `IgxHierarchicalGrid` diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts index 8a10cd7dfdd..e306d13a54b 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts @@ -590,7 +590,7 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti * const schema = this.grid.schema; * this.grid.schema = [{ name: 'Products', fields: [...], childEntities: [...] }]; * ``` - * @deprecated in version 21.0.0. + * @deprecated in version 20.2.0. */ @Input() public set schema(entities: EntityType[]) {} From 26a8fb598bed66300038f06253bbf0946e56f00e Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Wed, 11 Nov 2026 16:33:12 +0200 Subject: [PATCH 6/7] fix(h-grid): sync generated schema with column changes --- CHANGELOG.md | 2 + .../src/hierarchical-grid.component.ts | 46 +++++-- .../src/hierarchical-grid.spec.ts | 116 +++++++++++++++++- .../src/row-island.component.ts | 2 + 4 files changed, 156 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14402e815d2..419d525efc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,8 @@ All notable changes for each version of this project will be documented in this - `IgxHierarchicalGridComponent` - The advanced filtering dialog now lists the nested row islands of a row island whose child data is not available yet, e.g. when it is loaded on demand. Previously they were left out when the first record had no child data. - The fields of an auto-generated row island in the advanced filtering dialog are updated when its child grids generate their columns, e.g. when the child data is loaded on demand, and take the column `header`, `dataType` and formatting options set in `columnInit`. Previously they were resolved only from the first record and were empty when it had no child data. + - The fields of a row island with declared columns in the advanced filtering dialog now take the column `header`, `filters`, `editorOptions` and formatting options, as the fields of the root grid do. Previously only the `field` and the `dataType` were used. + - The fields in the advanced filtering dialog are updated when the columns of the root grid or of a row island are added, removed or changed after the dialog was first opened, e.g. when they are rendered conditionally or loaded later. - `IgxTreeGridComponent` - Setting `data` before the grid is initialized while `autoGenerate` is enabled no longer throws an error. In Ignite UI for Web Components this happened when `data` was set on `igc-tree-grid` before the element was attached to the page. - `IgxPivotGridComponent` diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts index 4ff61cb9254..8a328f90f71 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts @@ -1285,6 +1285,9 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti protected override onColumnsChanged() { Promise.resolve().then(() => { this.updateColumnList(); + if (!this.parent) { + this.updateGeneratedSchemaFields(); + } }); } @@ -1343,7 +1346,11 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti } private getFilterableFields(columns: IgxColumnComponent[]): FieldType[] { - return columns.filter((column) => !column.columnGroup && column.filterable).map(f => ({ + return this.toFieldTypes(columns.filter((column) => !column.columnGroup && column.filterable)); + } + + private toFieldTypes(columns: IgxColumnComponent[]): FieldType[] { + return columns.map(f => ({ field: f.field, dataType: f.dataType, header: f.header, @@ -1362,27 +1369,53 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti */ private updateGeneratedSchemaFields() { const root = this.rootGrid as IgxHierarchicalGridComponent; - if (root._schemaSetByUser || !root._hGridSchema?.length) { + root.setGeneratedSchemaFields(this.parentIsland, this.getFilterableFields(this.columns)); + } + + /** + * Updates the fields of the generated schema entity that corresponds to a row island with declared columns, + * so that columns added, removed or changed after the schema was created are reflected + * in the advanced filtering dialog. A schema set by the user is not modified. + * + * @hidden @internal + */ + public updateRowIslandSchemaFields(rowIsland: IgxRowIslandComponent) { + if (!rowIsland.autoGenerate) { + this.setGeneratedSchemaFields(rowIsland, this.getRowIslandFields(rowIsland)); + } + } + + /** Sets the fields of the generated schema entity of a row island, or of the root entity when no row island is passed. */ + private setGeneratedSchemaFields(rowIsland: IgxRowIslandComponent | null | undefined, fields: FieldType[]) { + if (this._schemaSetByUser || !this._hGridSchema?.length) { return; } const path: string[] = []; - let island = this.parentIsland; + let island = rowIsland; while (island) { path.unshift(island.key); island = island.parentIsland; } - let entity: EntityType | undefined = root._hGridSchema[0]; + let entity: EntityType | undefined = this._hGridSchema[0]; for (const key of path) { entity = entity?.childEntities?.find(e => e.name === key); } if (entity) { - entity.fields = this.getFilterableFields(this.columns); + entity.fields = fields; } } + /** + * Resolves the fields of a row island with declared columns, including the ones in column groups. + * Columns that are not filterable are kept, so that expression trees that already use them can still be recreated. + */ + private getRowIslandFields(rowIsland: IgxRowIslandComponent): FieldType[] { + return this.toFieldTypes(flatten(rowIsland.childColumns.toArray()).filter(col => col.field)); + } + private generateChildEntity(rowIsland: IgxRowIslandComponent, firstRowData: any): EntityType { const entityName = rowIsland.key; let fields: any[] = []; @@ -1391,8 +1424,7 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti const childGrid = rowIsland.autoGenerate && rowIsland.rowIslandAPI.getChildGrids().find(grid => grid.columns.length > 0); if (!rowIsland.autoGenerate) { - fields = flatten(rowIsland.childColumns.toArray()).filter(col => col.field) - .map(f => ({ field: f.field, dataType: f.dataType })) as FieldType[]; + fields = this.getRowIslandFields(rowIsland); } else if (childGrid) { fields = this.getFilterableFields(childGrid.columns); } else if (firstRowData) { diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts index 8e89268ae7e..f972130c772 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts @@ -2809,8 +2809,10 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { expect(contacts.name).toBe('contacts'); expect(toFieldTypes(contacts)).toEqual([ { field: 'Email', dataType: 'string' }, - { field: 'Phone', dataType: 'number' } + { field: 'Phone', dataType: 'number' }, + { field: 'Fax', dataType: 'string' } ]); + expect(contacts.fields[0].header).toBe('E-mail'); expect(contacts.childEntities).toBeUndefined(); }); @@ -2872,6 +2874,104 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { expect(orders.fields[1].header).toBe('Order Total'); }); + it('should update the fields of the generated schema when the columns of the grid change', async () => { + const schema = hierarchicalGrid.schema; + expect(toFieldTypes(schema[0])).toEqual([ + { field: 'ID', dataType: 'number' }, + { field: 'ReleaseDate', dataType: 'date' } + ]); + + fixture.componentInstance.showDiscontinued = true; + fixture.detectChanges(); + await fixture.whenStable(); + + expect(hierarchicalGrid.schema).toBe(schema); + expect(toFieldTypes(schema[0])).toEqual([ + { field: 'ID', dataType: 'number' }, + { field: 'ReleaseDate', dataType: 'date' }, + { field: 'Discontinued', dataType: 'boolean' } + ]); + + fixture.componentInstance.showDiscontinued = false; + fixture.detectChanges(); + await fixture.whenStable(); + + expect(toFieldTypes(schema[0])).toEqual([ + { field: 'ID', dataType: 'number' }, + { field: 'ReleaseDate', dataType: 'date' } + ]); + }); + + it('should update the fields of row islands with declared columns when their columns change', async () => { + const schema = hierarchicalGrid.schema; + const contacts = schema[0].childEntities[1]; + expect(toFieldTypes(contacts)).toEqual([ + { field: 'Email', dataType: 'string' }, + { field: 'Phone', dataType: 'number' }, + { field: 'Fax', dataType: 'string' } + ]); + + fixture.componentInstance.showAddress = true; + fixture.detectChanges(); + await fixture.whenStable(); + + expect(hierarchicalGrid.schema).toBe(schema); + expect(schema[0].childEntities[1]).toBe(contacts); + expect(toFieldTypes(contacts)).toEqual([ + { field: 'Email', dataType: 'string' }, + { field: 'Phone', dataType: 'number' }, + { field: 'Fax', dataType: 'string' }, + { field: 'Address', dataType: 'string' } + ]); + + fixture.componentInstance.showAddress = false; + fixture.detectChanges(); + await fixture.whenStable(); + + expect(toFieldTypes(contacts)).toEqual([ + { field: 'Email', dataType: 'string' }, + { field: 'Phone', dataType: 'number' }, + { field: 'Fax', dataType: 'string' } + ]); + }); + + it('should not modify a schema set by the user when the columns of a row island change', async () => { + const contactsFields: FieldType[] = [{ field: 'Email', dataType: 'string' }]; + const schema: EntityType[] = [{ + name: 'Products', + fields: [{ field: 'ID', dataType: 'number' }], + childEntities: [{ name: 'contacts', fields: contactsFields }] + }]; + hierarchicalGrid.schema = schema; + + fixture.componentInstance.showAddress = true; + fixture.detectChanges(); + await fixture.whenStable(); + + expect(schema[0].childEntities[0].fields).toBe(contactsFields); + expect(contactsFields).toEqual([{ field: 'Email', dataType: 'string' }]); + }); + + it('should apply an expression tree that uses a non-filterable column of a row island with declared columns', () => { + const tree = { + operator: 0, entity: 'root', returnFields: ['ID'], + filteringOperands: [{ + fieldName: 'ID', conditionName: 'inQuery', ignoreCase: false, + searchTree: { + operator: 0, entity: 'contacts', returnFields: ['Email'], + filteringOperands: [{ fieldName: 'Fax', conditionName: 'contains', ignoreCase: true, searchVal: '45' }] + } + }] + }; + + expect(() => { + hierarchicalGrid.advancedFilteringExpressionsTree = JSON.parse(JSON.stringify(tree)); + fixture.detectChanges(); + }).not.toThrow(); + const [operand] = hierarchicalGrid.advancedFilteringExpressionsTree.filteringOperands as any[]; + expect(operand.searchTree.filteringOperands[0].condition.name).toBe('contains'); + }); + it('should use a schema set by the user instead of generating one', () => { const schema: EntityType[] = [{ name: 'Products', fields: [{ field: 'ID', dataType: 'number' }] }]; @@ -3663,14 +3763,21 @@ export class IgxHierarchicalGridRowIslandTemplatesComponent extends IgxHierarchi + @if (showDiscontinued) { + + } - + + + @if (showAddress) { + + } `, changeDetection: ChangeDetectionStrategy.Eager, @@ -3680,11 +3787,14 @@ export class IgxHierarchicalGridSchemaComponent { @ViewChild('hierarchicalGrid', { read: IgxHierarchicalGridComponent, static: true }) public hgrid: IgxHierarchicalGridComponent; + public showDiscontinued = false; + public showAddress = false; + public data: any[] = [ { ID: 1, Name: 'Product A', ReleaseDate: new Date(2020, 1, 1), orders: [{ OrderID: 10, Total: 25.5, Shipped: true, items: [{ ItemID: 100, Name: 'Item' }] }], - contacts: [{ Email: 'contact@example.com', Phone: 123 }] + contacts: [{ Email: 'contact@example.com', Phone: 123, Fax: '456', Address: 'Address' }] } ]; } diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/row-island.component.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/row-island.component.ts index fa48be783cc..29cf18d2f2c 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/row-island.component.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/row-island.component.ts @@ -34,6 +34,7 @@ import { IgxHierarchicalGridBaseDirective } from './hierarchical-grid-base.direc import { IGridResourceStrings } from 'igniteui-angular/core'; import { first, filter, takeUntil, pluck } from 'rxjs/operators'; import { IgxRowIslandAPIService } from './row-island-api.service'; +import type { IgxHierarchicalGridComponent } from './hierarchical-grid.component'; import { IGridCreatedEventArgs } from './events'; import { IgxPaginatorComponent, IgxPaginatorDirective } from 'igniteui-angular/paginator'; import { IForOfState } from 'igniteui-angular/directives'; @@ -510,6 +511,7 @@ export class IgxRowIslandComponent extends IgxHierarchicalGridBaseDirective grid.updateOnRender = true; } }); + (this.rootGrid as unknown as IgxHierarchicalGridComponent)?.updateRowIslandSchemaFields(this); } protected updateChildren() { From 1b45c0643f4de09f605b3aef509633e1dc31c93a Mon Sep 17 00:00:00 2001 From: igdmdimitrov Date: Thu, 12 Nov 2026 13:55:03 +0200 Subject: [PATCH 7/7] fix(h-grid): keep adv filtering schema in sync with columns --- CHANGELOG.md | 2 +- .../src/hierarchical-grid.component.ts | 54 +++++++-- .../src/hierarchical-grid.spec.ts | 111 +++++++++++++++++- .../src/row-island.component.ts | 2 +- 4 files changed, 156 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaca3b4f967..4ec70b3c377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,7 +127,7 @@ All notable changes for each version of this project will be documented in this - Multi-row layout cell borders stay visible when the row is hovered or selected. - `IgxHierarchicalGridComponent` - The advanced filtering dialog now lists the nested row islands of a row island whose child data is not available yet, e.g. when it is loaded on demand. Previously they were left out when the first record had no child data. - - The fields of an auto-generated row island in the advanced filtering dialog are updated when its child grids generate their columns, e.g. when the child data is loaded on demand, and take the column `header`, `dataType` and formatting options set in `columnInit`. Previously they were resolved only from the first record and were empty when it had no child data. + - The fields of an auto-generated row island in the advanced filtering dialog are updated when its child grids generate their columns, e.g. when the child data is loaded on demand, and take the column `header`, `dataType` and formatting options set in `columnInit`. The fields of its nested row islands that are still empty are resolved from the loaded child data. Previously they were resolved only from the first record and were empty when it had no child data. - The fields of a row island with declared columns in the advanced filtering dialog now take the column `header`, `filters`, `editorOptions` and formatting options, as the fields of the root grid do. Previously only the `field` and the `dataType` were used. - The fields in the advanced filtering dialog are updated when the columns of the root grid or of a row island are added, removed or changed after the dialog was first opened, e.g. when they are rendered conditionally or loaded later. - `IgxTreeGridComponent` diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts index 8a328f90f71..8662f580133 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts @@ -1264,7 +1264,7 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti this.createColumnsList(this.parentIsland.childColumns.toArray()); } else { super.setupColumns(); - if (this.autoGenerate) { + if (this.autoGenerate && this.columns.length > 0) { this.updateGeneratedSchemaFields(); } } @@ -1369,7 +1369,35 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti */ private updateGeneratedSchemaFields() { const root = this.rootGrid as IgxHierarchicalGridComponent; - root.setGeneratedSchemaFields(this.parentIsland, this.getFilterableFields(this.columns)); + const fields = this.parentIsland ? this.getChildGridFields(this.columns) : this.getFilterableFields(this.columns); + const entity = root.setGeneratedSchemaFields(this.parentIsland, fields); + const firstRecord = this.data?.[0]; + if (entity && this.parentIsland && firstRecord) { + // nested row islands that have no fields yet (e.g. their parent data is loaded on demand) + // are resolved from the first record of this grid, as they are when the schema is generated + root.fillEmptySchemaFields(entity, root.generateChildEntity(this.parentIsland, firstRecord)); + } + } + + /** Sets the fields of the entities that have none from the matching entities of another entity tree. */ + private fillEmptySchemaFields(target: EntityType, source: EntityType) { + if (!target.fields?.length) { + target.fields = source.fields; + } + source.childEntities?.forEach(sourceChild => { + const targetChild = target.childEntities?.find(e => e.name === sourceChild.name); + if (targetChild) { + this.fillEmptySchemaFields(targetChild, sourceChild); + } + }); + } + + /** + * Resolves the fields of an auto-generated row island from the columns of one of its child grids. + * Columns that are not filterable are kept, so that expression trees that already use them can still be recreated. + */ + private getChildGridFields(columns: IgxColumnComponent[]): FieldType[] { + return this.toFieldTypes(columns.filter((column) => !column.columnGroup && column.field)); } /** @@ -1385,10 +1413,13 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti } } - /** Sets the fields of the generated schema entity of a row island, or of the root entity when no row island is passed. */ - private setGeneratedSchemaFields(rowIsland: IgxRowIslandComponent | null | undefined, fields: FieldType[]) { + /** + * Sets the fields of the generated schema entity of a row island, or of the root entity when no row island is passed. + * Returns the updated entity, if any. + */ + private setGeneratedSchemaFields(rowIsland: IgxRowIslandComponent | null | undefined, fields: FieldType[]): EntityType | undefined { if (this._schemaSetByUser || !this._hGridSchema?.length) { - return; + return undefined; } const path: string[] = []; @@ -1406,6 +1437,7 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti if (entity) { entity.fields = fields; } + return entity; } /** @@ -1420,25 +1452,27 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti const entityName = rowIsland.key; let fields: any[] = []; let childEntities; - // a child grid that is already created has its columns auto-generated from the actual child data + // a child grid that is already created has its columns auto-generated from the actual child data, + // with the header, data type and formatting options set in columnInit const childGrid = rowIsland.autoGenerate && rowIsland.rowIslandAPI.getChildGrids().find(grid => grid.columns.length > 0); + const childGridFields = childGrid ? this.getChildGridFields(childGrid.columns) : []; if (!rowIsland.autoGenerate) { fields = this.getRowIslandFields(rowIsland); - } else if (childGrid) { - fields = this.getFilterableFields(childGrid.columns); } else if (firstRowData) { const rowIslandFields = Object.keys(firstRowData).map(key => { if (firstRowData[key] instanceof Array) { return null; } - return { + return childGridFields.find(f => f.field === key) ?? { field: key, dataType: this.resolveDataTypes(firstRowData[key]) - } + }; }); fields = rowIslandFields.filter(f => f !== null) as FieldType[]; + } else { + fields = childGridFields; } // the child entities are generated from the row islands even if there is no child data yet (e.g. load on demand) diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts index f972130c772..8466a9d283b 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts @@ -2773,6 +2773,16 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { let fixture: ComponentFixture; let hierarchicalGrid: IgxHierarchicalGridComponent; const toFieldTypes = (entity: EntityType) => entity.fields.map(f => ({ field: f.field, dataType: f.dataType })); + const ordersSubQueryTree = (field: string, conditionName: string, searchVal: any) => ({ + operator: 0, entity: 'root', returnFields: ['ID'], + filteringOperands: [{ + fieldName: 'ID', conditionName: 'inQuery', ignoreCase: false, + searchTree: { + operator: 0, entity: 'orders', returnFields: ['OrderID'], + filteringOperands: [{ fieldName: field, conditionName, ignoreCase: true, searchVal }] + } + }] + }); beforeEach(() => { fixture = TestBed.createComponent(IgxHierarchicalGridSchemaComponent); @@ -2852,6 +2862,33 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { ]); }); + it('should resolve the empty fields of nested row islands from the data of a child grid that generates columns', async () => { + fixture.componentInstance.data = [ + { ID: 1, Name: 'Product A', ReleaseDate: new Date(), orders: [], contacts: [] }, + { + ID: 2, Name: 'Product B', ReleaseDate: new Date(), contacts: [], + orders: [{ OrderID: 20, Total: 10, items: [{ ItemID: 200, Name: 'Item' }] }] + } + ]; + fixture.detectChanges(); + const [orders] = hierarchicalGrid.schema[0].childEntities; + const [items] = orders.childEntities; + expect(orders.fields).toEqual([]); + expect(items.fields).toEqual([]); + + hierarchicalGrid.expandRow(fixture.componentInstance.data[1]); + fixture.detectChanges(); + await fixture.whenStable(); + + const childGrids = hierarchicalGrid.gridAPI.getChildGrids(true) as IgxHierarchicalGridComponent[]; + expect(childGrids.map(grid => grid.parentIsland.key)).not.toContain('items'); + expect(orders.childEntities[0]).toBe(items); + expect(toFieldTypes(items)).toEqual([ + { field: 'ItemID', dataType: 'number' }, + { field: 'Name', dataType: 'string' } + ]); + }); + it('should resolve the fields of auto-generated row islands from the columns of the created child grids', async () => { fixture.componentInstance.data = [ { ID: 1, Name: 'Product A', ReleaseDate: new Date(), orders: [], contacts: [] }, @@ -2972,6 +3009,71 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { expect(operand.searchTree.filteringOperands[0].condition.name).toBe('contains'); }); + it('should keep the fields of an auto-generated row island when a row without child data is expanded', async () => { + fixture.componentInstance.data = [ + { ID: 1, Name: 'Product A', ReleaseDate: new Date(), orders: [{ OrderID: 10, Total: 5, Shipped: true }], contacts: [] }, + { ID: 2, Name: 'Product B', ReleaseDate: new Date(), orders: [], contacts: [] } + ]; + fixture.detectChanges(); + const [orders] = hierarchicalGrid.schema[0].childEntities; + expect(toFieldTypes(orders)).toEqual([ + { field: 'OrderID', dataType: 'number' }, + { field: 'Total', dataType: 'number' }, + { field: 'Shipped', dataType: 'boolean' } + ]); + + hierarchicalGrid.expandRow(fixture.componentInstance.data[1]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(hierarchicalGrid.gridAPI.getChildGrids()[0].columns.length).toBe(0); + + expect(toFieldTypes(orders)).toEqual([ + { field: 'OrderID', dataType: 'number' }, + { field: 'Total', dataType: 'number' }, + { field: 'Shipped', dataType: 'boolean' } + ]); + expect(() => { + hierarchicalGrid.advancedFilteringExpressionsTree = JSON.parse(JSON.stringify(ordersSubQueryTree('Total', 'greaterThan', 1))); + fixture.detectChanges(); + }).not.toThrow(); + }); + + it('should keep the non-filterable columns of an auto-generated row island in its fields', async () => { + fixture.componentInstance.nonFilterableOrdersColumns = ['Shipped']; + const [orders] = hierarchicalGrid.schema[0].childEntities; + expect(toFieldTypes(orders).map(f => f.field)).toEqual(['OrderID', 'Total', 'Shipped']); + + hierarchicalGrid.expandRow(fixture.componentInstance.data[0]); + fixture.detectChanges(); + await fixture.whenStable(); + const childGrid = hierarchicalGrid.gridAPI.getChildGrids()[0]; + expect(childGrid.columns.map(c => c.filterable)).toEqual([true, true, false]); + + expect(toFieldTypes(orders).map(f => f.field)).toEqual(['OrderID', 'Total', 'Shipped']); + expect(() => { + hierarchicalGrid.advancedFilteringExpressionsTree = JSON.parse(JSON.stringify(ordersSubQueryTree('Shipped', 'true', null))); + fixture.detectChanges(); + }).not.toThrow(); + }); + + it('should resolve the fields of an auto-generated row island from the new child records after the data is replaced', async () => { + hierarchicalGrid.expandRow(fixture.componentInstance.data[0]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(hierarchicalGrid.gridAPI.getChildGrids()[0].columns.map(c => c.field)).toEqual(['OrderID', 'Total', 'Shipped']); + + fixture.componentInstance.data = [{ ID: 5, Name: 'Product C', ReleaseDate: new Date(), orders: [{ Code: 'a', Qty: 1 }], contacts: [] }]; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const [orders] = hierarchicalGrid.schema[0].childEntities; + expect(toFieldTypes(orders)).toEqual([ + { field: 'Code', dataType: 'string' }, + { field: 'Qty', dataType: 'number' } + ]); + }); + it('should use a schema set by the user instead of generating one', () => { const schema: EntityType[] = [{ name: 'Products', fields: [{ field: 'ID', dataType: 'number' }] }]; @@ -3766,7 +3868,7 @@ export class IgxHierarchicalGridRowIslandTemplatesComponent extends IgxHierarchi @if (showDiscontinued) { } - + @@ -3789,6 +3891,7 @@ export class IgxHierarchicalGridSchemaComponent { public showDiscontinued = false; public showAddress = false; + public nonFilterableOrdersColumns: string[] = []; public data: any[] = [ { @@ -3797,6 +3900,12 @@ export class IgxHierarchicalGridSchemaComponent { contacts: [{ Email: 'contact@example.com', Phone: 123, Fax: '456', Address: 'Address' }] } ]; + + public ordersColumnInit(column: IgxColumnComponent) { + if (this.nonFilterableOrdersColumns.includes(column.field)) { + column.filterable = false; + } + } } @Component({ diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/row-island.component.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/row-island.component.ts index 29cf18d2f2c..b632776765d 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/row-island.component.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/row-island.component.ts @@ -511,7 +511,7 @@ export class IgxRowIslandComponent extends IgxHierarchicalGridBaseDirective grid.updateOnRender = true; } }); - (this.rootGrid as unknown as IgxHierarchicalGridComponent)?.updateRowIslandSchemaFields(this); + (this.rootGrid as IgxHierarchicalGridComponent)?.updateRowIslandSchemaFields(this); } protected updateChildren() {