diff --git a/CHANGELOG.md b/CHANGELOG.md index bf83fc33a69..4ec70b3c377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes for each version of this project will be documented in this ### General - **Browser support** - The minimum supported browser versions are now documented: Chrome and Edge 131, Firefox 151, and Safari 18 on macOS and iOS. The component styles rely on CSS container style queries and relative colors, so older browsers, including Firefox ESR 140, are not supported. See the [Browser Support](README.md#browser-support) section of the README. +- `IgxHierarchicalGridComponent` + - **Deprecation** - The `schema` input has been deprecated and will be removed in a future version. Define the columns and the row islands of the grid instead, the advanced filtering schema is generated from them. A `schema` that is set is still used as before. ### New Functionality @@ -123,6 +125,11 @@ All notable changes for each version of this project will be documented in this - Autosizing a child column of a column layout now fits its content, instead of keeping the width of its layout area. - When grouping is applied and no column is pinned at the start, the group-by indentation and the summary area below it use the pinned border. - 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`. 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` - 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/grid/src/grid-filtering-advanced.spec.ts b/projects/igniteui-angular/grids/grid/src/grid-filtering-advanced.spec.ts index 804ec975a48..54639681d65 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-filtering-advanced.spec.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-filtering-advanced.spec.ts @@ -1851,6 +1851,49 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { expect(dropdownValues).toEqual(expectedValues); }); + it('Should have proper fields in UI when auto-generated child data is loaded on demand without a schema.', async () => { + const fixture = TestBed.createComponent(IgxHGridAutoGenerateOnDemandComponent); + const hierarchicalGrid = fixture.componentInstance.hGrid; + hierarchicalGrid.allowAdvancedFiltering = true; + fixture.detectChanges(); + + // There is no child data yet, but the child entities are generated from the row islands. + const [childEntity] = hierarchicalGrid.schema[0].childEntities; + expect(childEntity.name).toBe('childData'); + expect(childEntity.fields).toEqual([]); + expect(childEntity.childEntities.map(e => e.name)).toEqual(['childData2']); + + // Load the child data on demand. + hierarchicalGrid.expandRow('0'); + fixture.detectChanges(); + await fixture.componentInstance.childDataLoaded; + fixture.detectChanges(); + await fixture.whenStable(); + + hierarchicalGrid.openAdvancedFilteringDialog(); + fixture.detectChanges(); + + // Click the initial 'Add Condition' button. + QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fixture, 0); + await fixture.whenStable(); + fixture.detectChanges(); + // Populate edit inputs. + await QueryBuilderFunctions.selectColumnInEditModeExpression(fixture, 0); // Select 'ID' column. + await QueryBuilderFunctions.selectOperatorInEditModeExpression(fixture, 10); // Select 'In' operator. + await fixture.whenStable(); + fixture.detectChanges(); + + const entityInputGroup = QueryBuilderFunctions.getQueryBuilderEntitySelect(fixture, 1).querySelector('input'); + expect(entityInputGroup.value).toBe('childData'); + + // Verify return fields + const queryBuilderElement: HTMLElement = fixture.debugElement.queryAll(By.css(`.${QueryBuilderSelectors.QUERY_BUILDER_TREE}`))[1].nativeElement; + QueryBuilderFunctions.clickQueryBuilderFieldsCombo(fixture, 1); + fixture.detectChanges(); + const dropdownValues = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement, 0).map((x: any) => x.innerText); + expect(dropdownValues).toEqual(['ID', 'ProductName', 'Price']); + }); + it('Should correctly change resource strings for hierarchical Advanced Filtering dialog.', async () => { hgrid.closeAdvancedFilteringDialog(false); await fix.whenStable(); @@ -2029,6 +2072,37 @@ class IgxHGridRemoteOnDemandComponent { } } +@Component({ + template: ` + + + + + `, + changeDetection: ChangeDetectionStrategy.Eager, + imports: [IgxHierarchicalGridComponent, IgxRowIslandComponent] +}) +class IgxHGridAutoGenerateOnDemandComponent { + @ViewChild('hGrid', { read: IgxHierarchicalGridComponent, static: true }) + public hGrid: IgxHierarchicalGridComponent; + + public data = [ + { ID: '0', ProductName: 'Product: A0' }, + { ID: '1', ProductName: 'Product: A1' } + ]; + + /** Resolved when the on demand child data is assigned to the created child grid. */ + public childDataLoaded: Promise; + + public gridCreated(event: IGridCreatedEventArgs) { + this.childDataLoaded = new Promise(resolve => setTimeout(() => { + event.grid.data = [{ ID: 10, ProductName: 'Child: A0', Price: 5 }]; + event.grid.cdr.detectChanges(); + resolve(); + })); + } +} + @Component({ template: ` = new Map(); @@ -581,16 +582,21 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti * @Input() * @param {EntityType[]} entities - An array of EntityType objects representing the grid's schema. * @remarks - * This property is required in remote data filtering scenarios. + * When not set, the schema is generated from the filterable columns of the grid and from the row islands. + * The fields of auto-generated row islands are resolved from the child data and are updated when + * the child grids generate their columns, e.g. when the child data is loaded on demand. * @example * ```typescript * const schema = this.grid.schema; * this.grid.schema = [{ name: 'Products', fields: [...], childEntities: [...] }]; * ``` + * @deprecated in version 22.3.0. Define the columns and the row islands of the grid instead, + * the schema is generated from them. */ @Input() public set schema(entities: EntityType[]) { this._hGridSchema = entities; + this._schemaSetByUser = !!entities; } /* blazorSuppress */ @@ -1258,6 +1264,9 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti this.createColumnsList(this.parentIsland.childColumns.toArray()); } else { super.setupColumns(); + if (this.autoGenerate && this.columns.length > 0) { + this.updateGeneratedSchemaFields(); + } } } @@ -1276,6 +1285,9 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti protected override onColumnsChanged() { Promise.resolve().then(() => { this.updateColumnList(); + if (!this.parent) { + this.updateGeneratedSchemaFields(); + } }); } @@ -1311,23 +1323,14 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti } private generateSchema() { - const filterableFields = this.columns.filter((column) => !column.columnGroup && column.filterable); + const filterableFields = this.getFilterableFields(this.columns); let entities!: EntityType[]; if(filterableFields.length !== 0) { entities = [ { name: null!, - fields: filterableFields.map(f => ({ - field: f.field, - dataType: f.dataType, - header: f.header, - editorOptions: f.editorOptions, - filters: f.filters, - pipeArgs: f.pipeArgs, - defaultTimeFormat: f.defaultTimeFormat, - defaultDateTimeFormat: f.defaultDateTimeFormat - })) as FieldType[] + fields: filterableFields } ]; @@ -1342,37 +1345,144 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti return entities; } + private getFilterableFields(columns: IgxColumnComponent[]): FieldType[] { + 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, + editorOptions: f.editorOptions, + filters: f.filters, + pipeArgs: f.pipeArgs, + defaultTimeFormat: f.defaultTimeFormat, + defaultDateTimeFormat: f.defaultDateTimeFormat + })) as FieldType[]; + } + + /** + * Updates the fields of the generated schema entity that corresponds to this grid with its current columns, + * so that columns auto-generated after the schema was created (e.g. for child data loaded on demand) + * become available in the advanced filtering dialog. A schema set by the user is not modified. + */ + private updateGeneratedSchemaFields() { + const root = this.rootGrid as IgxHierarchicalGridComponent; + 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)); + } + + /** + * 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. + * Returns the updated entity, if any. + */ + private setGeneratedSchemaFields(rowIsland: IgxRowIslandComponent | null | undefined, fields: FieldType[]): EntityType | undefined { + if (this._schemaSetByUser || !this._hGridSchema?.length) { + return undefined; + } + + const path: string[] = []; + let island = rowIsland; + while (island) { + path.unshift(island.key); + island = island.parentIsland; + } + + let entity: EntityType | undefined = this._hGridSchema[0]; + for (const key of path) { + entity = entity?.childEntities?.find(e => e.name === key); + } + + if (entity) { + entity.fields = fields; + } + return entity; + } + + /** + * 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[] = []; let childEntities; + // 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 = flatten(rowIsland.childColumns.toArray()).filter(col => col.field) - .map(f => ({ field: f.field, dataType: f.dataType })) as FieldType[]; + fields = this.getRowIslandFields(rowIsland); } 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) const rowIslandChildEntities = rowIsland.childLayoutList.reduce((acc, childRowIsland) => { - if (!firstRowData) { - return null; - } - const childFirstRowData = firstRowData[childRowIsland.key]?.length > 0 ? + const childFirstRowData = firstRowData?.[childRowIsland.key]?.length > 0 ? firstRowData[childRowIsland.key][0] : null; return acc.concat(this.generateChildEntity(childRowIsland, childFirstRowData)); - }, [] as any); + }, [] as EntityType[]); - if (rowIslandChildEntities?.length > 0) { + if (rowIslandChildEntities.length > 0) { childEntities = rowIslandChildEntities; } 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 80a01ba8525..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 @@ -15,7 +15,7 @@ import { IgxExcelStyleSortingComponent } from 'igniteui-angular/grids/core'; import { IgxExcelStyleSearchComponent } from 'igniteui-angular/grids/core'; import { IgxCellHeaderTemplateDirective } from 'igniteui-angular/grids/core'; import { advance, setElementSize, useFakeClock } from '../../../test-utils/helper-utils.spec'; -import { ColumnType, EntityType, IgxOverlayOutletDirective, IgxStringFilteringOperand, ɵSize, getComponentSize } from 'igniteui-angular/core'; +import { ColumnType, EntityType, FieldType, IgxOverlayOutletDirective, IgxStringFilteringOperand, ɵSize, getComponentSize } from 'igniteui-angular/core'; import { IgxIconComponent } from 'igniteui-angular/icon'; import { IgxDialogComponent } from 'igniteui-angular/dialog'; import { IGridCreatedEventArgs } from './events'; @@ -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); @@ -2809,8 +2819,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(); }); @@ -2822,7 +2834,244 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { expect(orders.name).toBe('orders'); expect(orders.fields).toEqual([]); - expect(orders.childEntities).toBeUndefined(); + // the child entities are generated from the nested row islands even without child data (e.g. load on demand) + expect(orders.childEntities.length).toBe(1); + expect(orders.childEntities[0].name).toBe('items'); + expect(orders.childEntities[0].fields).toEqual([]); + }); + + it('should update the fields of auto-generated row islands when their child grids generate columns', async () => { + fixture.componentInstance.data = [ + { ID: 1, Name: 'Product A', ReleaseDate: new Date(), orders: [], contacts: [] }, + { ID: 2, Name: 'Product B', ReleaseDate: new Date(), orders: [{ OrderID: 20, Total: 10, Shipped: false }], contacts: [] } + ]; + fixture.detectChanges(); + const schema = hierarchicalGrid.schema; + expect(schema[0].childEntities[0].fields).toEqual([]); + + hierarchicalGrid.expandRow(fixture.componentInstance.data[1]); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(hierarchicalGrid.schema).toBe(schema); + const [orders] = hierarchicalGrid.schema[0].childEntities; + expect(toFieldTypes(orders)).toEqual([ + { field: 'OrderID', dataType: 'number' }, + { field: 'Total', dataType: 'number' }, + { field: 'Shipped', dataType: 'boolean' } + ]); + }); + + 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: [] }, + { ID: 2, Name: 'Product B', ReleaseDate: new Date(), orders: [{ OrderID: 20, Total: 10 }], contacts: [] } + ]; + fixture.detectChanges(); + hierarchicalGrid.expandRow(fixture.componentInstance.data[1]); + fixture.detectChanges(); + await fixture.whenStable(); + + const childGrid = hierarchicalGrid.gridAPI.getChildGrids()[0]; + childGrid.getColumnByName('Total').header = 'Order Total'; + childGrid.getColumnByName('Total').dataType = 'currency'; + + const [orders] = hierarchicalGrid.schema[0].childEntities; + expect(toFieldTypes(orders)).toEqual([ + { field: 'OrderID', dataType: 'number' }, + { field: 'Total', dataType: 'currency' } + ]); + 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 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', () => { @@ -2832,6 +3081,25 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { expect(hierarchicalGrid.schema).toBe(schema); }); + + it('should not modify a schema set by the user when child grids generate columns', async () => { + const ordersFields: FieldType[] = [{ field: 'OrderID', dataType: 'number' }]; + const schema: EntityType[] = [{ + name: 'Products', + fields: [{ field: 'ID', dataType: 'number' }], + childEntities: [{ name: 'orders', fields: ordersFields }] + }]; + hierarchicalGrid.schema = schema; + + hierarchicalGrid.expandRow(fixture.componentInstance.data[0]); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(hierarchicalGrid.gridAPI.getChildGrids()[0].columns.length).toBeGreaterThan(1); + expect(hierarchicalGrid.schema).toBe(schema); + expect(schema[0].childEntities[0].fields).toBe(ordersFields); + expect(ordersFields).toEqual([{ field: 'OrderID', dataType: 'number' }]); + }); }); describe('IgxHierarchicalGrid Column Layouts #hGrid', () => { @@ -3597,14 +3865,21 @@ export class IgxHierarchicalGridRowIslandTemplatesComponent extends IgxHierarchi - + @if (showDiscontinued) { + + } + - + + + @if (showAddress) { + + } `, changeDetection: ChangeDetectionStrategy.Eager, @@ -3614,13 +3889,23 @@ export class IgxHierarchicalGridSchemaComponent { @ViewChild('hierarchicalGrid', { read: IgxHierarchicalGridComponent, static: true }) public hgrid: IgxHierarchicalGridComponent; + public showDiscontinued = false; + public showAddress = false; + public nonFilterableOrdersColumns: string[] = []; + 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' }] } ]; + + 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 fa48be783cc..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 @@ -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 IgxHierarchicalGridComponent)?.updateRowIslandSchemaFields(this); } protected updateChildren() {