diff --git a/CHANGELOG.md b/CHANGELOG.md index f1cd562f37e..b85ee771af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,13 @@ All notable changes for each version of this project will be documented in this - A column's `minWidth` and `maxWidth` constrain the user-specified `width` so that it cannot go outside their bounds. - 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). - In SSR mode grid with width 100% or with no width will render on the server with % size and with all columns. +- `IgxHierarchicalGrid` + - 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. + - Added a new `schema` input property that can be used to pass collection of `EntityType` objects. This property is required for remote data scenarios. +- `IgxQueryBuilderComponent`, `IgxAdvancedFilteringDialogComponent` + - Added support for entities with hierarchical structure. +- `EntityType` + - A new optional property called `childEntities` has been introduced that can be used to create nested entities. ## 19.1.1 ### New Features @@ -30,7 +37,7 @@ All notable changes for each version of this project will be documented in this - 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. - 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. - 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. -- `IgxQueryBuilder` has new design that comes with updated appearance and new functionality +- `IgxQueryBuilder` has a new design that comes with an updated appearance and new functionality - `IgxQueryBuilderComponent` - Introduced the ability to create nested queries by specifying IN/NOT IN operators. - Introduced the ability to reposition condition chips by dragging or using `Arrow Up/Down`. diff --git a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts index 6056d49ce84..0eb64660797 100644 --- a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts +++ b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts @@ -7,20 +7,20 @@ import { } from "../../../igniteui-angular/src/public_api"; import { IgxPaginatorComponent } from "../../../igniteui-angular/src/lib/paginator/paginator.component"; import { IgxPaginatorToken } from "../../../igniteui-angular/src/lib/paginator/token"; -import { IgxActionStripComponent } from "../../../igniteui-angular/src/lib/action-strip/action-strip.component"; -import { IgxActionStripToken } from "../../../igniteui-angular/src/lib/action-strip/token"; -import { IgxGridEditingActionsComponent } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component"; -import { IgxGridActionsBaseDirective } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-actions-base.directive"; -import { IgxGridPinningActionsComponent } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-pinning-actions.component"; -import { IgxColumnComponent } from "../../../igniteui-angular/src/lib/grids/columns/column.component"; -import { IgxColumnGroupComponent } from "../../../igniteui-angular/src/lib/grids/columns/column-group.component"; -import { IgxColumnLayoutComponent } from "../../../igniteui-angular/src/lib/grids/columns/column-layout.component"; import { IgxGridToolbarTitleComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/common"; import { IgxGridToolbarActionsComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/common"; import { IgxGridToolbarAdvancedFilteringComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/grid-toolbar-advanced-filtering.component"; import { IgxGridToolbarComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/grid-toolbar.component"; import { IgxToolbarToken } from "../../../igniteui-angular/src/lib/grids/toolbar/token"; +import { IgxColumnComponent } from "../../../igniteui-angular/src/lib/grids/columns/column.component"; +import { IgxColumnGroupComponent } from "../../../igniteui-angular/src/lib/grids/columns/column-group.component"; import { IgxRowIslandComponent } from "../../../igniteui-angular/src/lib/grids/hierarchical-grid/row-island.component"; +import { IgxActionStripComponent } from "../../../igniteui-angular/src/lib/action-strip/action-strip.component"; +import { IgxActionStripToken } from "../../../igniteui-angular/src/lib/action-strip/token"; +import { IgxGridEditingActionsComponent } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-editing-actions.component"; +import { IgxGridActionsBaseDirective } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-actions-base.directive"; +import { IgxGridPinningActionsComponent } from "../../../igniteui-angular/src/lib/action-strip/grid-actions/grid-pinning-actions.component"; +import { IgxColumnLayoutComponent } from "../../../igniteui-angular/src/lib/grids/columns/column-layout.component"; import { IgxGridToolbarExporterComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/grid-toolbar-exporter.component"; import { IgxGridToolbarHidingComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/grid-toolbar-hiding.component"; import { IgxGridToolbarPinningComponent } from "../../../igniteui-angular/src/lib/grids/toolbar/grid-toolbar-pinning.component"; diff --git a/projects/igniteui-angular/src/lib/data-operations/expressions-tree-util.ts b/projects/igniteui-angular/src/lib/data-operations/expressions-tree-util.ts index c9543362999..365f349d603 100644 --- a/projects/igniteui-angular/src/lib/data-operations/expressions-tree-util.ts +++ b/projects/igniteui-angular/src/lib/data-operations/expressions-tree-util.ts @@ -178,8 +178,9 @@ export function isTree(entry: IExpressionTree | IFilteringExpression): entry is * @param entities An array of entities to use for recreating the tree. * @returns The recreated expression tree. */ -export function recreateTree(tree: IExpressionTree, entities: EntityType[]): IExpressionTree { - const entity = entities.find(e => e.name === tree.entity); +export function recreateTree(tree: IExpressionTree, entities: EntityType[], isRoot: boolean = false): IExpressionTree { + const entity = isRoot ? entities[0] : entities.find(e => e.name === tree.entity); + if (!entity) return tree; for (let i = 0; i < tree.filteringOperands.length; i++) { const operand = tree.filteringOperands[i]; @@ -187,7 +188,7 @@ export function recreateTree(tree: IExpressionTree, entities: EntityType[]): IEx tree.filteringOperands[i] = recreateTree(operand, entities); } else { if (operand.searchTree) { - operand.searchTree = recreateTree(operand.searchTree, entities); + operand.searchTree = recreateTree(operand.searchTree, entities[0].childEntities ?? entities); } tree.filteringOperands[i] = recreateExpression(operand, entity?.fields); } diff --git a/projects/igniteui-angular/src/lib/data-operations/filtering-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/filtering-strategy.ts index 37a5759e231..bc8812b713b 100644 --- a/projects/igniteui-angular/src/lib/data-operations/filtering-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/filtering-strategy.ts @@ -1,12 +1,13 @@ import { FilteringLogic, IFilteringExpression } from './filtering-expression.interface'; import { FilteringExpressionsTree, IFilteringExpressionsTree } from './filtering-expressions-tree'; import { resolveNestedPath, parseDate, formatDate, formatCurrency } from '../core/utils'; -import { ColumnType, GridType } from '../grids/common/grid.interface'; +import { ColumnType, EntityType, GridType } from '../grids/common/grid.interface'; import { GridColumnDataType } from './data-util'; import { SortingDirection } from './sorting-strategy'; import { formatNumber, formatPercent, getLocaleCurrencyCode } from '@angular/common'; import { IFilteringState } from './filtering-state.interface'; import { isTree } from './expressions-tree-util'; +import { IgxHierarchicalGridComponent } from '../grids/hierarchical-grid/hierarchical-grid.component'; const DateType = 'date'; const DateTimeType = 'dateTime'; @@ -39,6 +40,26 @@ export interface IgxFilterItem { export abstract class BaseFilteringStrategy implements IFilteringStrategy { // protected public findMatchByExpression(rec: any, expr: IFilteringExpression, isDate?: boolean, isTime?: boolean, grid?: GridType): boolean { + if (expr.searchTree) { + const records = rec[expr.searchTree.entity]; + const shouldMatchRecords = expr.conditionName === 'inQuery'; + if (!records) { // child grid is not yet created + return true; + } else if (records.length === 0) { // child grid is empty + return false; + } + + for (let index = 0; index < records.length; index++) { + const record = records[index]; + if ((shouldMatchRecords && this.matchRecord(record, expr.searchTree, grid, expr.searchTree.entity)) || + (!shouldMatchRecords && !this.matchRecord(record, expr.searchTree, grid, expr.searchTree.entity))) { + return true; + } + } + + return false; + } + const val = this.getFieldValue(rec, expr.fieldName, isDate, isTime, grid); if (expr.condition?.logic) { return expr.condition.logic(val, expr.searchVal, expr.ignoreCase); @@ -46,7 +67,7 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy { } // protected - public matchRecord(rec: any, expressions: IFilteringExpressionsTree | IFilteringExpression, grid?: GridType): boolean { + public matchRecord(rec: any, expressions: IFilteringExpressionsTree | IFilteringExpression, grid?: GridType, entity?: string): boolean { if (expressions) { if (isTree(expressions)) { const expressionsTree = expressions; @@ -55,7 +76,7 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy { if (expressionsTree.filteringOperands && expressionsTree.filteringOperands.length) { for (const operand of expressionsTree.filteringOperands) { - matchOperand = this.matchRecord(rec, operand, grid); + matchOperand = this.matchRecord(rec, operand, grid, entity); // Return false if at least one operand does not match and the filtering logic is And if (!matchOperand && operator === FilteringLogic.And) { @@ -74,9 +95,19 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy { return true; } else { const expression = expressions; - const column = grid && grid.getColumnByName(expression.fieldName); - const isDate = column ? column.dataType === DateType || column.dataType === DateTimeType : false; - const isTime = column ? column.dataType === TimeType : false; + let dataType = null; + if (!entity) { + const column = grid && grid.getColumnByName(expression.fieldName); + dataType = column?.dataType; + } else if (grid.type === 'hierarchical') { + const schema = (grid as IgxHierarchicalGridComponent).schema; + const entityMatch = this.findEntityByName(schema, entity); + dataType = entityMatch?.fields.find(f => f.field === expression.fieldName)?.dataType; + } + + const isDate = dataType ? dataType === DateType || dataType === DateTimeType : false; + const isTime = dataType ? dataType === TimeType : false; + return this.findMatchByExpression(rec, expression, isDate, isTime, grid); } } @@ -84,6 +115,22 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy { return true; } + private findEntityByName(schema: EntityType[], name: string): EntityType | null { + for (const entity of schema) { + if (entity.name === name) { + return entity; + } + + if (entity.childEntities && entity.childEntities.length > 0) { + const found = this.findEntityByName(entity.childEntities, name); + if (found) { + return found; + } + } + } + return null; + } + public getFilterItems(column: ColumnType, tree: IFilteringExpressionsTree): Promise { let data = column.grid.gridAPI.filterDataByExpressions(tree); diff --git a/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts b/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts index f0ea80c2def..3c8d8aa4f38 100644 --- a/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts +++ b/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts @@ -1497,4 +1497,5 @@ export interface IClipboardOptions { export interface EntityType { name: string; fields: FieldType[]; + childEntities?: EntityType[]; } diff --git a/projects/igniteui-angular/src/lib/grids/filtering/advanced-filtering/advanced-filtering-dialog.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/advanced-filtering/advanced-filtering-dialog.component.ts index 3a733ddd475..e98253b688d 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/advanced-filtering/advanced-filtering-dialog.component.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/advanced-filtering/advanced-filtering-dialog.component.ts @@ -6,7 +6,7 @@ import { IDragStartEventArgs, IgxDragDirective, IgxDragHandleDirective } from '. import { Subject } from 'rxjs'; import { IActiveNode } from '../../grid-navigation.service'; import { PlatformUtil } from '../../../core/utils'; -import { FieldType, GridType } from '../../common/grid.interface'; +import { EntityType, FieldType, GridType } from '../../common/grid.interface'; import { IgxQueryBuilderComponent } from '../../../query-builder/query-builder.component'; import { GridResourceStringsEN } from '../../../core/i18n/grid-resources'; import { IFilteringExpressionsTree } from '../../../data-operations/filtering-expressions-tree'; @@ -15,6 +15,8 @@ import { IgxQueryBuilderHeaderComponent } from '../../../query-builder/query-bui import { NgClass } from '@angular/common'; import { getCurrentResourceStrings } from '../../../core/i18n/resources'; import { QueryBuilderResourceStringsEN } from '../../../core/i18n/query-builder-resources'; +import { IgxHierarchicalGridComponent } from '../../hierarchical-grid/hierarchical-grid.component'; +import { IgxRowIslandComponent } from '../../hierarchical-grid/row-island.component'; /** * A component used for presenting advanced filtering UI for a Grid. @@ -191,18 +193,33 @@ export class IgxAdvancedFilteringDialogComponent implements AfterViewInit, OnDes this.closeDialog(); } - /** * @hidden @internal */ public generateEntity() { - const entities = [ - { - name: null, - fields: this.filterableFields - } - ]; - return entities; + if (this.queryBuilder?.entities) { + return this.queryBuilder?.entities; + } else if (this.grid.type === 'hierarchical') { + return (this.grid as IgxHierarchicalGridComponent).schema; + } else { + const entities: EntityType[] = [ + { + name: null, + fields: this.filterableFields.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[] + } + ]; + return entities; + } } private assignResourceStrings() { diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index 065a9d00b19..42abc796055 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -147,7 +147,8 @@ import { ISizeInfo, RowType, IPinningConfig, - IClipboardOptions + IClipboardOptions, + EntityType } from './common/grid.interface'; import { DropPosition } from './moving/moving.service'; import { IgxHeadSelectorDirective, IgxRowSelectorDirective } from './selection/row-selectors'; @@ -179,7 +180,7 @@ import { DefaultDataCloneStrategy, IDataCloneStrategy } from '../data-operations import { IgxGridCellComponent } from './cell.component'; import { IgxGridValidationService } from './grid/grid-validation.service'; import { getCurrentResourceStrings } from '../core/i18n/resources'; -import { isTree, recreateTreeFromFields } from '../data-operations/expressions-tree-util'; +import { isTree, recreateTree, recreateTreeFromFields } from '../data-operations/expressions-tree-util'; import { getUUID } from './common/random'; interface IMatchInfoCache { @@ -1859,7 +1860,7 @@ export abstract class IgxGridBaseDirective implements GridType, value.type = FilteringExpressionsTreeType.Regular; if (value && this._columns?.length > 0) { - this._filteringExpressionsTree = recreateTreeFromFields(value, this._columns) as IFilteringExpressionsTree; + this._filteringExpressionsTree = this.getRecreatedTree(value); } else { this._filteringExpressionsTree = value; } @@ -1909,7 +1910,7 @@ export abstract class IgxGridBaseDirective implements GridType, if (value && isTree(value)) { value.type = FilteringExpressionsTreeType.Advanced; if (this._columns && this._columns.length > 0) { - this._advancedFilteringExpressionsTree = recreateTreeFromFields(value, this._columns) as IFilteringExpressionsTree; + this._advancedFilteringExpressionsTree = this.getRecreatedTree(value); } else { this._advancedFilteringExpressionsTree = value; } @@ -3138,6 +3139,7 @@ export abstract class IgxGridBaseDirective implements GridType, matchCount: 0, content: '' }; + protected _hGridSchema: EntityType[]; protected gridComputedStyles; /** @hidden @internal */ @@ -6638,10 +6640,10 @@ export abstract class IgxGridBaseDirective implements GridType, this._unpinnedColumns = newColumns.filter((c) => !c.pinned); this._columns = newColumns; if (this._columns && this._columns.length && this._filteringExpressionsTree) { - this._filteringExpressionsTree = recreateTreeFromFields(this._filteringExpressionsTree, this.columns) as IFilteringExpressionsTree; + this._filteringExpressionsTree = this.getRecreatedTree(this._filteringExpressionsTree); } if (this._columns && this._columns.length && this._advancedFilteringExpressionsTree) { - this._advancedFilteringExpressionsTree = recreateTreeFromFields(this._advancedFilteringExpressionsTree, this.columns) as IFilteringExpressionsTree; + this._advancedFilteringExpressionsTree = this.getRecreatedTree(this._advancedFilteringExpressionsTree); } this.resetCaches(); } @@ -6706,10 +6708,10 @@ export abstract class IgxGridBaseDirective implements GridType, this._columns = this.getColumnList(); } if (this._columns && this._columns.length && this._filteringExpressionsTree) { - this._filteringExpressionsTree = recreateTreeFromFields(this._filteringExpressionsTree, this._columns) as IFilteringExpressionsTree; + this._filteringExpressionsTree = this.getRecreatedTree(this._filteringExpressionsTree); } if (this._columns && this._columns.length && this._advancedFilteringExpressionsTree) { - this._advancedFilteringExpressionsTree = recreateTreeFromFields(this._advancedFilteringExpressionsTree, this._columns) as IFilteringExpressionsTree; + this._advancedFilteringExpressionsTree = this.getRecreatedTree(this._advancedFilteringExpressionsTree); } this.initColumns(this._columns, (col: IgxColumnComponent) => this.columnInit.emit(col)); @@ -7922,4 +7924,12 @@ export abstract class IgxGridBaseDirective implements GridType, this.navigation.activeNode = {} as IActiveNode; this.notifyChanges(); } + + private getRecreatedTree(value: IFilteringExpressionsTree): IFilteringExpressionsTree { + if (this._hGridSchema) { + return recreateTree(value, this._hGridSchema, true) as IFilteringExpressionsTree; + } else { + return recreateTreeFromFields(value, this._columns) as IFilteringExpressionsTree; + } + } } 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 9fb2b661cfe..8e591807f24 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 @@ -20,7 +20,7 @@ import { IgxGridAdvancedFilteringWithToolbarComponent } from '../../test-utils/grid-samples.spec'; import { FormattedValuesFilteringStrategy } from '../../data-operations/filtering-strategy'; -import { IgxHierGridExternalAdvancedFilteringComponent } from '../../test-utils/hierarchical-grid-components.spec'; +import { IgxHierarchicalGridExportComponent, IgxHierarchicalGridTestBaseComponent, IgxHierGridExternalAdvancedFilteringComponent } from '../../test-utils/hierarchical-grid-components.spec'; import { IgxHierarchicalGridComponent } from '../hierarchical-grid/public_api'; import { IFilteringEventArgs, IgxGridToolbarAdvancedFilteringComponent } from '../public_api'; import { SampleTestData } from '../../test-utils/sample-test-data.spec'; @@ -28,6 +28,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'; describe('IgxGrid - Advanced Filtering #grid - ', () => { configureTestSuite((() => { @@ -40,7 +41,10 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { IgxGridAdvancedFilteringBindingComponent, IgxHierGridExternalAdvancedFilteringComponent, IgxGridAdvancedFilteringDynamicColumnsComponent, - IgxGridAdvancedFilteringWithToolbarComponent + IgxGridAdvancedFilteringWithToolbarComponent, + IgxHierarchicalGridTestBaseComponent, + IgxHierarchicalGridExportComponent, + IgxHGridRemoteOnDemandComponent ] }); })); @@ -637,9 +641,9 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { fix.detectChanges(); const dropdownItems = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement); expect(dropdownItems.length).toBe(4); - expect((dropdownItems[0] as HTMLElement).innerText).toBe('HeaderID'); + expect((dropdownItems[0] as HTMLElement).innerText).toBe('ID'); expect((dropdownItems[1] as HTMLElement).innerText).toBe('ProductName'); - expect((dropdownItems[2] as HTMLElement).innerText).toBe('Another Field'); + expect((dropdownItems[2] as HTMLElement).innerText).toBe('AnotherField'); expect((dropdownItems[3] as HTMLElement).innerText).toBe('ReleaseTime'); })); @@ -1292,7 +1296,7 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { QueryBuilderFunctions.selectColumnInEditModeExpression(fix, 1); // Select 'ProductName' column. QueryBuilderFunctions.selectOperatorInEditModeExpression(fix, 0); // Select 'Contains' operator. - let input = QueryBuilderFunctions.getQueryBuilderValueInput(fix).querySelector('input'); + const input = QueryBuilderFunctions.getQueryBuilderValueInput(fix).querySelector('input'); UIInteractions.clickAndSendInputElementValue(input, 'angular', fix); // Type filter value. // Commit the populated expression. QueryBuilderFunctions.clickQueryBuilderExpressionCommitButton(fix); @@ -1328,7 +1332,7 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { QueryBuilderFunctions.clickQueryBuilderColumnSelect(fix); fix.detectChanges(); const dropdownValues = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement).map((x: any) => x.innerText); - const expectedValues = ['ID', 'ProductName', 'Downloads', 'Released', 'ReleaseDate', 'Another Field', 'DateTimeCreated']; + const expectedValues = ['ID', 'ProductName', 'Downloads', 'Released', 'ReleaseDate', 'AnotherField', 'DateTimeCreated']; expect(expectedValues).toEqual(dropdownValues); })); }); @@ -1455,11 +1459,17 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { }); describe('Expression tree rehydration - ', () => { + let fix: ComponentFixture; + let grid: IgxGridComponent; + + beforeEach(fakeAsync(() => { + fix = TestBed.createComponent(IgxGridAdvancedFilteringSerializedTreeComponent); + grid = fix.componentInstance.grid; + fix.detectChanges(); + })); + it('should correctly filter with a deserialized expression tree.', fakeAsync(() => { const errorSpy = spyOn(console, 'error'); - let fix = TestBed.createComponent(IgxGridAdvancedFilteringSerializedTreeComponent); - fix.detectChanges(); - let grid = fix.componentInstance.grid; expect(errorSpy).not.toHaveBeenCalled(); @@ -1470,12 +1480,8 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { it('should correctly filter with a declared IFilteringExpressionsTree object.', fakeAsync(() => { const errorSpy = spyOn(console, 'error'); - let fix = TestBed.createComponent(IgxGridAdvancedFilteringSerializedTreeComponent); - fix.detectChanges(); fix.componentInstance.grid.advancedFilteringExpressionsTree = fix.componentInstance.filterTreeObject; fix.detectChanges(); - let grid = fix.componentInstance.grid; - expect(errorSpy).not.toHaveBeenCalled(); // Verify filtered data @@ -1485,11 +1491,8 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { it('should correctly filter when binding to a declared IFilteringExpressionsTree object.', fakeAsync(() => { const errorSpy = spyOn(console, 'error'); - let fix = TestBed.createComponent(IgxGridAdvancedFilteringSerializedTreeComponent); - fix.detectChanges(); fix.componentInstance.filterTree = fix.componentInstance.filterTreeObject; fix.detectChanges(); - let grid = fix.componentInstance.grid; expect(errorSpy).not.toHaveBeenCalled(); @@ -1498,6 +1501,299 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { expect(grid.rowList.length).toBe(2); })); }); + + describe('Hierarchical grid advanced filtering - ', () => { + let fix: ComponentFixture; + let hgrid: IgxHierarchicalGridComponent; + + beforeEach(fakeAsync(() => { + fix = TestBed.createComponent(IgxHierarchicalGridTestBaseComponent); + hgrid = fix.componentInstance.hgrid; + hgrid.allowAdvancedFiltering = true; + fix.detectChanges(); + + // Open Advanced Filtering dialog. + hgrid.openAdvancedFilteringDialog(); + fix.detectChanges(); + + // Click the initial 'Add Condition' button of the query builder. + QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fix, 0); + tick(100); + fix.detectChanges(); + })); + + it(`Should have 'In'/'Not-In' operators for fields with chilld entities.`, fakeAsync(() => { + // Populate edit inputs. + QueryBuilderFunctions.selectColumnInEditModeExpression(fix, 0); // Select 'ID' column. + + // Open the operator dropdown and verify they are 'string' specific + 'In'/'Not In'. + QueryBuilderFunctions.clickQueryBuilderOperatorSelect(fix); + fix.detectChanges(); + const queryBuilderElement: HTMLElement = fix.debugElement.queryAll(By.css(`.${QueryBuilderSelectors.QUERY_BUILDER_TREE}`))[0].nativeElement; + const dropdownValues: string[] = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement).map((x: any) => x.innerText); + const expectedValues = ['Contains', 'Does Not Contain', 'Starts With', 'Ends With', 'Equals', + 'Does Not Equal', 'Empty', 'Not Empty', 'Null', 'Not Null', 'In', 'Not In'];; + expect(dropdownValues).toEqual(expectedValues); + + // Close Advanced Filtering dialog. + hgrid.closeAdvancedFilteringDialog(false); + tick(200); + fix.detectChanges(); + })); + + it(`Should NOT have 'In'/'Not-In' operators for fields without chilld entities.`, fakeAsync(() => { + // Populate edit inputs. + QueryBuilderFunctions.selectColumnInEditModeExpression(fix, 0); // Select 'ID' column. + QueryBuilderFunctions.selectOperatorInEditModeExpression(fix, 10); // Select 'In' operator. + + // Select entity in nested level + QueryBuilderFunctions.selectEntityAndClickInitialAddCondition(fix, 0, 1); + // Populate edit inputs on level 1. + QueryBuilderFunctions.selectColumnInEditModeExpression(fix, 0, 1); // Select 'ID' column. + QueryBuilderFunctions.selectOperatorInEditModeExpression(fix, 11, 1); // Select 'Not In' operator. + + // Select entity in nested level + QueryBuilderFunctions.selectEntityAndClickInitialAddCondition(fix, 0, 2); + QueryBuilderFunctions.selectColumnInEditModeExpression(fix, 0, 2); // Select 'ID' column. + // Open the operator dropdown and verify they are 'string' specific + 'In'/'Not In'. + QueryBuilderFunctions.clickQueryBuilderOperatorSelect(fix, 2); + fix.detectChanges(); + const queryBuilderElement: HTMLElement = fix.debugElement.queryAll(By.css(`.${QueryBuilderSelectors.QUERY_BUILDER_TREE}`))[2].nativeElement; + const dropdownValues: string[] = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement).map((x: any) => x.innerText); + const expectedValues = ['Contains', 'Does Not Contain', 'Starts With', 'Ends With', 'Equals', + 'Does Not Equal', 'Empty', 'Not Empty', 'Null', 'Not Null'];; + expect(dropdownValues).toEqual(expectedValues); + + // Close Advanced Filtering dialog. + hgrid.closeAdvancedFilteringDialog(false); + tick(200); + fix.detectChanges(); + })); + + it('Should have correct entities depending on the hierarchy level.', fakeAsync(() => { + // Populate edit inputs. + QueryBuilderFunctions.selectColumnInEditModeExpression(fix, 0); // Select 'ID' column. + QueryBuilderFunctions.selectOperatorInEditModeExpression(fix, 10); // Select 'In' operator. + + QueryBuilderFunctions.clickQueryBuilderEntitySelect(fix, 1); + fix.detectChanges(); + const queryBuilderElement: HTMLElement = fix.debugElement.queryAll(By.css(`.${QueryBuilderSelectors.QUERY_BUILDER_TREE}`))[1].nativeElement; + const dropdownValues: string[] = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement).map((x: any) => x.innerText); + const expectedValues = ['childData']; + expect(dropdownValues).toEqual(expectedValues); + + // Close Advanced Filtering dialog. + hgrid.closeAdvancedFilteringDialog(false); + tick(200); + fix.detectChanges(); + })); + + it(`Should apply 'In'/'Not-In' operators for each level properly.`, fakeAsync(() => { + // Populate edit inputs. + QueryBuilderFunctions.selectColumnInEditModeExpression(fix, 0); // Select 'ID' column. + QueryBuilderFunctions.selectOperatorInEditModeExpression(fix, 10); // Select 'In' operator. + tick(100); + fix.detectChanges(); + + // When there is one entity, it should be selected by default + const entityInputGroup = QueryBuilderFunctions.getQueryBuilderEntitySelect(fix, 1).querySelector('input'); + expect(entityInputGroup.value).toBe('childData'); + + const fieldInputGroup = QueryBuilderFunctions.getQueryBuilderFieldsCombo(fix, 1).querySelector('input'); + expect(fieldInputGroup.value).toBe('ID'); + + // Click the initial 'Add Condition' button. + QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fix, 0); + tick(100); + fix.detectChanges(); + // Populate edit inputs on level 1. + QueryBuilderFunctions.selectColumnInEditModeExpression(fix, 0, 1); // Select 'ID' column. + QueryBuilderFunctions.selectOperatorInEditModeExpression(fix, 0, 1); // Select 'Contains' operator. + + const input = QueryBuilderFunctions.getQueryBuilderValueInput(fix, false, 1).querySelector('input'); + // Type Value + UIInteractions.clickAndSendInputElementValue(input, '39'); + tick(100); + fix.detectChanges(); + + QueryBuilderFunctions.clickQueryBuilderExpressionCommitButton(fix, 1); + fix.detectChanges(); + QueryBuilderFunctions.clickQueryBuilderExpressionCommitButton(fix, 0); + fix.detectChanges(); + // Close Advanced Filtering dialog. + hgrid.closeAdvancedFilteringDialog(true); + tick(200); + fix.detectChanges(); + + // Veify grid data + expect(hgrid.filteredData.length).toEqual(5); + expect(hgrid.rowList.length).toBe(5); + })); + + it(`Should have correct return fields in the child query when there are multiple child entities.`, fakeAsync(() => { + const fixture = TestBed.createComponent(IgxHierarchicalGridExportComponent); + const hierarchicalGrid = fixture.componentInstance.hGrid; + fixture.componentInstance.shouldDisplayArtist = true; + hierarchicalGrid.allowAdvancedFiltering = true; + 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 'Artist' column. + QueryBuilderFunctions.selectOperatorInEditModeExpression(fixture, 10); // Select 'In' operator. + tick(100); + fixture.detectChanges(); + + QueryBuilderFunctions.selectEntityInEditModeExpression(fixture, 0, 1); + tick(100); + fixture.detectChanges(); + + const fieldInputGroup = QueryBuilderFunctions.getQueryBuilderFieldsCombo(fixture, 1).querySelector('input'); + expect(fieldInputGroup.value).toBe('Artist'); + })); + + it('Should correctly apply filtering expressions tree to the hgrid component through API.', fakeAsync(() => { + // Close Advanced Filtering dialog. + hgrid.closeAdvancedFilteringDialog(false); + tick(200); + fix.detectChanges(); + // Spy for error messages in the console + const consoleSpy = spyOn(console, 'error'); + // Apply advanced filter through API. + const innerTree = new FilteringExpressionsTree(0, undefined, 'childData', ['ID']); + innerTree.filteringOperands.push({ + fieldName: 'ID', + ignoreCase: false, + conditionName: IgxStringFilteringOperand.instance().condition('contains').name, + searchVal: '39' + }); + + const tree = new FilteringExpressionsTree(0, undefined, 'rootData', ['ID']); + tree.filteringOperands.push({ + fieldName: 'ID', + conditionName: IgxStringFilteringOperand.instance().condition('inQuery').name, + ignoreCase: false, + searchTree: innerTree + }); + + hgrid.advancedFilteringExpressionsTree = tree; + fix.detectChanges(); + + // Check for error messages in the console + expect(consoleSpy).not.toHaveBeenCalled(); + expect(hgrid.filteredData.length).toBe(5); + })); + + it('Should correctly apply JSON filtering expressions tree to the hgrid correctly.', fakeAsync(() => { + // Close Advanced Filtering dialog. + hgrid.closeAdvancedFilteringDialog(false); + tick(200); + fix.detectChanges(); + // Spy for error messages in the console + const consoleSpy = spyOn(console, 'error'); + + const innerTree = new FilteringExpressionsTree(0, undefined, 'childData', ['ID']); + innerTree.filteringOperands.push({ + fieldName: 'ID', + ignoreCase: false, + conditionName: IgxStringFilteringOperand.instance().condition('contains').name, + searchVal: '39' + }); + + const tree = new FilteringExpressionsTree(0, undefined, 'rootData', ['ID']); + tree.filteringOperands.push({ + fieldName: 'ID', + conditionName: IgxStringFilteringOperand.instance().condition('inQuery').name, + ignoreCase: false, + searchTree: innerTree + }); + + hgrid.advancedFilteringExpressionsTree = JSON.parse(JSON.stringify(tree)); + fix.detectChanges(); + + // Check for error messages in the console + expect(consoleSpy).not.toHaveBeenCalled(); + 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; + 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'); + + const fieldInputGroup = QueryBuilderFunctions.getQueryBuilderFieldsCombo(fixture, 1).querySelector('input'); + expect(fieldInputGroup.value).toBe('ID'); + + // 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 fileds + QueryBuilderFunctions.clickQueryBuilderFieldsCombo(fixture, 1); + fixture.detectChanges(); + dropdownValues = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement, 1).map((x: any) => x.innerText); + expectedValues = ['ID', 'ProductName']; + expect(dropdownValues).toEqual(expectedValues); + })); + }); }); 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 4b06865bfdf..4812af26798 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 @@ -35,7 +35,7 @@ import { takeUntil } from 'rxjs/operators'; import { IgxTemplateOutletDirective } from '../../directives/template-outlet/template_outlet.directive'; import { IgxGridSelectionService } from '../selection/selection.service'; import { IgxForOfSyncService, IgxForOfScrollSyncService } from '../../directives/for-of/for_of.sync.service'; -import { CellType, GridType, IGX_GRID_BASE, IGX_GRID_SERVICE_BASE, RowType } from '../common/grid.interface'; +import { CellType, EntityType, FieldType, GridType, IGX_GRID_BASE, IGX_GRID_SERVICE_BASE, RowType } from '../common/grid.interface'; import { IgxRowIslandAPIService } from './row-island-api.service'; import { IgxGridCRUDService } from '../common/crud.service'; import { IgxHierarchicalGridRow } from '../grid-public-row'; @@ -66,6 +66,8 @@ import { IgxGridDragSelectDirective } from '../selection/drag-select.directive'; import { IgxGridBodyDirective } from '../grid.common'; import { IgxGridHeaderRowComponent } from '../headers/grid-header-row.component'; import { IgxActionStripToken } from '../../action-strip/token'; +import { flatten } from '../../core/utils'; +import { IFilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree'; let NEXT_ID = 0; @@ -448,6 +450,17 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti return this.parentIsland ? this.parentIsland.actionStrip : super.actionStrip; } + public override get advancedFilteringExpressionsTree(): IFilteringExpressionsTree { + return super.advancedFilteringExpressionsTree; + } + + public override set advancedFilteringExpressionsTree(value: IFilteringExpressionsTree) { + if (!this._hGridSchema) { + this._hGridSchema = this.generateSchema(); + } + super.advancedFilteringExpressionsTree = value; + } + private _data; private h_id = `igx-hierarchical-grid-${NEXT_ID++}`; private childGridTemplates: Map = new Map(); @@ -561,6 +574,32 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti return this._defaultExpandState; } + /** + * Gets/Sets the schema for the hierarchical grid. + * This schema defines the structure and properties of the data displayed in the grid. + * @Input() + * @param {EntityType[]} entities - An array of EntityType objects representing the grid's schema. + * @remarks + * This property is required in remote data filtering scenarios. + * @example + * ```typescript + * const schema = this.grid.schema; + * this.grid.schema = [{ name: 'Products', fields: [...], childEntities: [...] }]; + * ``` + */ + @Input() + public set schema(entities: EntityType[]) { + this._hGridSchema = entities; + } + + public get schema() { + if (!this._hGridSchema) { + this._hGridSchema = this.generateSchema(); + } + + return this._hGridSchema; + } + /** * Gets the unique identifier of the parent row. It may be a `string` or `number` if `primaryKey` of the * parent grid is set or an object reference of the parent record otherwise. @@ -1191,4 +1230,74 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti grid.cdr.markForCheck(); }); } + + private generateSchema() { + const filterableFields = this.columns.filter((column) => !column.columnGroup && column.filterable); + let entities: EntityType[]; + + if(filterableFields.length !== 0) { + entities = [ + { + name: null, + fields: filterableFields.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[] + } + ]; + + entities[0].childEntities = this.childLayoutList.reduce((acc, rowIsland) => { + return acc.concat(this.generateChildEntity(rowIsland, this.data[0][rowIsland.key][0])); + } + , []); + } + + return entities; + } + + private generateChildEntity(rowIsland: IgxRowIslandComponent, firstRowData: any[]): EntityType { + const entityName = rowIsland.key; + let fields = []; + let childEntities; + if (!rowIsland.autoGenerate) { + 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])); + }, []); + + if (rowIslandChildEntities?.length > 0) { + childEntities = rowIslandChildEntities; + } + + return { + name: entityName, + fields: fields, + childEntities: childEntities + } + } } diff --git a/projects/igniteui-angular/src/lib/query-builder/query-builder-functions.spec.ts b/projects/igniteui-angular/src/lib/query-builder/query-builder-functions.spec.ts index e334816edc3..917a630ffaf 100644 --- a/projects/igniteui-angular/src/lib/query-builder/query-builder-functions.spec.ts +++ b/projects/igniteui-angular/src/lib/query-builder/query-builder-functions.spec.ts @@ -311,14 +311,14 @@ export class QueryBuilderFunctions { return outlet; } - public static getQueryBuilderSelectDropdown(queryBuilderElement: HTMLElement) { + public static getQueryBuilderSelectDropdown(queryBuilderElement: HTMLElement, index = 0) { const outlet = QueryBuilderFunctions.getQueryBuilderOutlet(queryBuilderElement); - const selectDropdown = outlet.querySelector(`.${QueryBuilderSelectors.DROP_DOWN_LIST_SCROLL}`); + const selectDropdown = outlet.querySelectorAll(`.${QueryBuilderSelectors.DROP_DOWN_LIST_SCROLL}`).item(index); return selectDropdown; } - public static getQueryBuilderSelectDropdownItems(queryBuilderElement: HTMLElement) { - const selectDropdown = QueryBuilderFunctions.getQueryBuilderSelectDropdown(queryBuilderElement); + public static getQueryBuilderSelectDropdownItems(queryBuilderElement: HTMLElement, index = 0) { + const selectDropdown = QueryBuilderFunctions.getQueryBuilderSelectDropdown(queryBuilderElement, index); const items = Array.from(selectDropdown.querySelectorAll('.igx-drop-down__item')); return items; } @@ -841,13 +841,13 @@ export class QueryBuilderFunctions { fix.detectChanges(); } - public static selectEntityAndClickInitialAddCondition(fix: ComponentFixture, entityIndex: number, groupIndex = 0) { - QueryBuilderFunctions.selectEntityInEditModeExpression(fix, entityIndex); + public static selectEntityAndClickInitialAddCondition(fix: ComponentFixture, entityIndex: number, level = 0) { + QueryBuilderFunctions.selectEntityInEditModeExpression(fix, entityIndex, level); tick(100); fix.detectChanges(); // Click the initial 'Add Condition' button. - QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fix, groupIndex); + QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fix, level); tick(100); fix.detectChanges(); } diff --git a/projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.html b/projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.html index 2197f888cd8..9ebaf1a9191 100644 --- a/projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.html +++ b/projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.html @@ -7,7 +7,7 @@ -
+
From
- Select + @if (!this.isHierarchicalNestedQuery()) { + Select + } @if (!parentExpression) { 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 0194fbd7549..7a59165e80c 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 @@ -194,8 +194,8 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { public set expressionTree(expressionTree: IExpressionTree) { this._expressionTree = expressionTree; if (!expressionTree) { - this._selectedEntity = null; - this._selectedReturnFields = []; + this._selectedEntity = this.isAdvancedFiltering() && this.entities.length === 1 ? this.entities[0] : null; + this._selectedReturnFields = this._selectedEntity ? this._selectedEntity.fields?.map(f => f.field) : []; } if (!this._preventInit) { @@ -242,6 +242,11 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { return this._resourceStrings; } + /** + * Gets/sets the expected return field. + */ + @Input() public expectedReturnField: string = null; + /** * Event fired as the expression tree is changed. */ @@ -516,7 +521,14 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { /** @hidden */ protected isAdvancedFiltering(): boolean { - return this.entities?.length === 1 && !this.entities[0]?.name; + return (this.entities?.length === 1 && !this.entities[0]?.name) || + this.entities?.find(e => e.childEntities?.length > 0) !== undefined || + this.entities !== this.queryBuilder?.entities; + } + + /** @hidden */ + protected isHierarchicalNestedQuery(): boolean { + return this.queryBuilder.entities !== this.entities } /** @hidden */ @@ -548,6 +560,14 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { this.returnFieldSelectOverlaySettings.outlet = this.overlayOutlet; this.addExpressionDropDownOverlaySettings.outlet = this.overlayOutlet; this.groupContextMenuDropDownOverlaySettings.outlet = this.overlayOutlet; + + if (this.isAdvancedFiltering() && this.entities?.length === 1) { + this.selectedEntity = this.entities[0].name; + if (this._selectedEntity.fields.find(f => f.field === this.expectedReturnField)) { + this._selectedReturnFields = [this.expectedReturnField]; + } + } + // Trigger additional change detection cycle this.cdr.detectChanges(); } @@ -617,7 +637,11 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { } this.fields = this._entityNewValue ? this._entityNewValue.fields : []; - this._selectedReturnFields = this.parentExpression ? [] : this._entityNewValue.fields?.map(f => f.field); + if (this._selectedEntity.fields.find(f => f.field === this.expectedReturnField)) { + this._selectedReturnFields = [this.expectedReturnField]; + } else { + this._selectedReturnFields = this.parentExpression ? [] : this._entityNewValue.fields?.map(f => f.field); + } if (this._expressionTree) { this._expressionTree.entity = this._entityNewValue.name; @@ -680,6 +704,10 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { if (this._selectedField !== value) { this._selectedField = value; + if (this._selectedField && !this._selectedField.dataType) { + this._selectedField.filters = this.getFilters(this._selectedField); + } + this.selectDefaultCondition(); if (oldValue && this._selectedField && this._selectedField.dataType !== oldValue.dataType) { this.searchValue.value = null; @@ -823,7 +851,10 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { if (innerQuery && this.selectedField?.filters?.condition(this.selectedCondition)?.isNestedQuery) { innerQuery.exitEditAddMode(); this._editedExpression.expression.searchTree = this.getExpressionTreeCopy(innerQuery.expressionTree); - this._editedExpression.expression.searchTree.returnFields = innerQuery.selectedReturnFields; + const returnFields = innerQuery.selectedReturnFields.length > 0 ? + innerQuery.selectedReturnFields : + [innerQuery.fields[0].field]; + this._editedExpression.expression.searchTree.returnFields = returnFields; } else { this._editedExpression.expression.searchTree = null; } @@ -837,6 +868,10 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { this._editedExpression = null; } + if (this.selectedReturnFields.length === 0) { + this.selectedReturnFields = this.fields.map(f => f.field); + } + this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup, this.selectedEntity?.name, this.selectedReturnFields); if (!this.parentExpression) { this.expressionTreeChange.emit(this._expressionTree); @@ -920,7 +955,6 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { */ public operandCanBeCommitted(): boolean { const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0]; - return this.selectedField && this.selectedCondition && ( ( @@ -933,7 +967,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { this.selectedField.filters.condition(this.selectedCondition)?.isUnary ); } - + /** * @hidden @internal */ @@ -1353,7 +1387,12 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { public getConditionList(): string[] { if (!this.selectedField) return []; - if (this.entities?.length === 1 && !this.entities[0].name) { + if (!this.selectedField.filters) { + this.selectedField.filters = this.getFilters(this.selectedField); + } + + if ((this.isAdvancedFiltering() && !this.entities[0].childEntities) || + (this.isHierarchicalNestedQuery() && this.selectedEntity.name && !this.selectedEntity.childEntities)) { return this.selectedField.filters.conditionList(); } @@ -1520,6 +1559,8 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { default: return IgxStringFilteringOperand.instance(); } + } else { + return field.filters; } } diff --git a/projects/igniteui-angular/src/lib/query-builder/query-builder.component.spec.ts b/projects/igniteui-angular/src/lib/query-builder/query-builder.component.spec.ts index 98be7fbf5a4..0800ba40c53 100644 --- a/projects/igniteui-angular/src/lib/query-builder/query-builder.component.spec.ts +++ b/projects/igniteui-angular/src/lib/query-builder/query-builder.component.spec.ts @@ -1887,10 +1887,7 @@ describe('IgxQueryBuilder', () => { tick(100); fix.detectChanges(); - commitBtn = QueryBuilderFunctions.getQueryBuilderExpressionCommitButton(fix); - ControlsFunction.verifyButtonIsDisabled(commitBtn as HTMLElement, true); - - // Select return field + // Change return field from preselected 'OrderId' to 'Id' QueryBuilderFunctions.selectFieldsInEditModeExpression(fix, [0], 1); tick(100); fix.detectChanges(); @@ -1901,7 +1898,7 @@ describe('IgxQueryBuilder', () => { QueryBuilderFunctions.verifyEditModeExpressionInputStates(fix, true, true, false, true); // Parent commit button should be enabled QueryBuilderFunctions.clickQueryBuilderExpressionCommitButton(fix); fix.detectChanges(); - + //Verify that expressionTree is correct const exprTree = JSON.stringify(fix.componentInstance.queryBuilder.expressionTree, null, 2); expect(exprTree).toBe(`{ diff --git a/projects/igniteui-angular/src/lib/test-utils/hierarchical-grid-components.spec.ts b/projects/igniteui-angular/src/lib/test-utils/hierarchical-grid-components.spec.ts index 6e611b87043..4e5afe2ca3f 100644 --- a/projects/igniteui-angular/src/lib/test-utils/hierarchical-grid-components.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/hierarchical-grid-components.spec.ts @@ -444,7 +444,7 @@ export class IgxHierarchicalGridActionStripComponent extends IgxHierarchicalGrid @Component({ template: ` - + @@ -459,13 +459,13 @@ export class IgxHierarchicalGridActionStripComponent extends IgxHierarchicalGrid - + `, imports: [IgxHierarchicalGridComponent, IgxColumnComponent, IgxRowIslandComponent, IgxAdvancedFilteringDialogComponent] }) export class IgxHierGridExternalAdvancedFilteringComponent extends IgxHierarchicalGridTestBaseComponent { - // @ViewChild('hierarchicalGrid', { read: IgxHierarchicalGridComponent, static: true }) - // public hgrid: IgxHierarchicalGridComponent; + @ViewChild('hierarchicalGrid', { read: IgxHierarchicalGridComponent, static: true }) + public override hgrid: IgxHierarchicalGridComponent; public override data = SampleTestData.generateHGridData(5, 3); } @@ -484,6 +484,9 @@ export class IgxHierGridExternalAdvancedFilteringComponent extends IgxHierarchic + @if(shouldDisplayArtist) { + + } @@ -510,6 +513,7 @@ export class IgxHierGridExternalAdvancedFilteringComponent extends IgxHierarchic export class IgxHierarchicalGridExportComponent { @ViewChild('hierarchicalGrid', { read: IgxHierarchicalGridComponent, static: true }) public hGrid: IgxHierarchicalGridComponent; public data = SampleTestData.hierarchicalGridExportData(); + public shouldDisplayArtist = false; } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4b1626262d1..c4637552c6b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -576,6 +576,10 @@ export class AppComponent implements OnInit { link: '/hierarchicalGrid', icon: 'view_column', name: 'Hierarchical Grid' + }, { + link: '/hierarchicalGridAdvancedFiltering', + icon: 'view_column', + name: 'Hierarchical Grid Advanced Filtering' }, { link: '/hierarchicalGridRemote', icon: 'swap_vert', diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 9ec076b2b3e..992516f1574 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -146,6 +146,7 @@ import { GridDockManagerSampleComponent } from './dockmanager-grid/dockmanager-g import { HoundComponent } from './hound/hound.component'; import { LabelSampleComponent } from "./label/label.sample"; import { GridRecreateSampleComponent } from './grid-re-create/grid-re-create.sample'; +import { HierarchicalGridAdvancedFilteringSampleComponent } from './hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample'; export const appRoutes: Routes = [ { @@ -616,6 +617,9 @@ export const appRoutes: Routes = [ }, { path: 'hierarchicalGrid', component: HierarchicalGridSampleComponent + }, { + path: 'hierarchicalGridAdvancedFiltering', + component: HierarchicalGridAdvancedFilteringSampleComponent }, { path: 'hierarchicalGridRemote', component: HierarchicalGridRemoteSampleComponent diff --git a/src/app/hierarchical-grid-advanced-filtering/data.ts b/src/app/hierarchical-grid-advanced-filtering/data.ts new file mode 100644 index 00000000000..9e002d38df9 --- /dev/null +++ b/src/app/hierarchical-grid-advanced-filtering/data.ts @@ -0,0 +1,1925 @@ + +/* eslint-disable id-blacklist */ +/* eslint-disable @typescript-eslint/naming-convention */ +export interface Song { + Number: number; + Title: string; + Released: Date; + Genre: string; + Album: string; +} +export interface Tour { + Tour: string; + StartedOn: string; + Location: string; + Headliner: string; + TouredBy: string; +} +export interface Album { + Album: string; + LaunchDate: Date; + BillboardReview: number; + USBillboard200: number; + Artist: string; + Songs?: Song[]; +} + +export interface Singer { + ID: number; + Artist: string; + // Photo?: string; + Debut: number; + GrammyNominations: number; + GrammyAwards: number; + HasGrammyAward: boolean; + Tours?: Tour[]; + Albums?: Album[]; +} + +// tslint:disable:object-literal-sort-keys +export const SINGERS: Singer[] = [ + { + ID: 0, + Artist: 'Naomí Yepes', + // Photo: 'assets/images/hgrid/naomi.jpg', + Debut: 2011, + GrammyNominations: 6, + GrammyAwards: 0, + HasGrammyAward: false, + Tours: [ + { + Tour: 'Faithful Tour', + StartedOn: 'Sep 12', + Location: 'Worldwide', + Headliner: 'NO', + TouredBy: 'Naomí Yepes' + }, + { + Tour: 'City Jam Sessions', + StartedOn: 'Aug 13', + Location: 'North America', + Headliner: 'YES', + TouredBy: 'Naomí Yepes' + }, + { + Tour: 'Christmas NYC 2013', + StartedOn: 'Dec 13', + Location: 'United States', + Headliner: 'NO', + TouredBy: 'Naomí Yepes' + }, + { + Tour: 'Christmas NYC 2014', + StartedOn: 'Dec 14', + Location: 'North America', + Headliner: 'NO', + TouredBy: 'Naomí Yepes' + }, + { + Tour: 'Watermelon Tour', + StartedOn: 'Feb 15', + Location: 'Worldwide', + Headliner: 'YES', + TouredBy: 'Naomí Yepes' + }, + { + Tour: 'Christmas NYC 2016', + StartedOn: 'Dec 16', + Location: 'United States', + Headliner: 'NO', + TouredBy: 'Naomí Yepes' + }, + { + Tour: 'The Dragon Tour', + StartedOn: 'Feb 17', + Location: 'Worldwide', + Headliner: 'NO', + TouredBy: 'Naomí Yepes' + }, + { + Tour: 'Organic Sessions', + StartedOn: 'Aug 18', + Location: 'United States, England', + Headliner: 'YES', + TouredBy: 'Naomí Yepes' + }, + { + Tour: 'Hope World Tour', + StartedOn: 'Mar 19', + Location: 'Worldwide', + Headliner: 'NO', + TouredBy: 'Naomí Yepes' + } + ], + Albums: [ + { + Album: 'Initiation', + LaunchDate: new Date('September 3, 2013'), + BillboardReview: 86, + USBillboard200: 1, + Artist: 'Naomí Yepes', + Songs: [{ + Number: 1, + Title: 'Ambitious', + Released: new Date('28 Apr 2015'), + Genre: 'Dance-pop R&B', + Album: 'Initiation' + }, + { + Number: 2, + Title: 'My heart will go on', + Released: new Date('24 May 2015'), + Genre: 'Dance-pop R&B', + Album: 'Initiation' + }, + { + Number: 3, + Title: 'Sing to me', + Released: new Date('28 May 2015'), + Genre: 'Dance-pop R&B', + Album: 'Initiation' + }, + { + Number: 4, + Title: 'Want to dance with somebody', + Released: new Date('03 Jun 2015'), + Genre: 'Dance-pop R&B', + Album: 'Initiation' + }] + }, + { + Album: 'Dream Driven', + LaunchDate: new Date('August 25, 2014'), + BillboardReview: 81, + USBillboard200: 1, + Artist: 'Naomí Yepes', + Songs: [{ + Number: 1, + Title: 'Intro', + Released: null, + Genre: '*', + Album: 'Dream Driven' + }, + { + Number: 2, + Title: 'Ferocious', + Released: new Date('28 Apr 2014'), + Genre: 'Dance-pop R&B', + Album: 'Dream Driven' + }, + { + Number: 3, + Title: 'Going crazy', + Released: new Date('10 Feb 2015'), + Genre: 'Dance-pop EDM', + Album: 'Dream Driven' + }, + { + Number: 4, + Title: 'Future past', + Released: null, + Genre: '*', + Album: 'Dream Driven' + }, + { + Number: 5, + Title: 'Roaming like them', + Released: new Date('2 Jul 2014'), + Genre: 'Electro house Electropop', + Album: 'Dream Driven' + }, + { + Number: 6, + Title: 'Last Wishes', + Released: new Date('12 Aug 2014'), + Genre: 'R&B', + Album: 'Dream Driven' + }, + { + Number: 7, + Title: 'Stay where you are', + Released: null, + Genre: '*', + Album: 'Dream Driven' + }, + { + Number: 8, + Title: 'Imaginarium', + Released: null, + Genre: '*', + Album: 'Dream Driven' + }, + { + Number: 9, + Title: 'Tell me', + Released: new Date('30 Sep 2014'), + Genre: 'Synth-pop R&B', + Album: 'Dream Driven' + }, + { + Number: 10, + Title: 'Shredded into pieces', + Released: null, + Genre: '*', + Album: 'Dream Driven' + }, + { + Number: 11, + Title: 'Capture this moment', + Released: null, + Genre: '*', + Album: 'Dream Driven' + }, + { + Number: 12, + Title: 'Dream Driven', + Released: null, + Genre: '*', + Album: 'Dream Driven' + }] + }, + { + Album: 'The dragon journey', + LaunchDate: new Date('May 20, 2016'), + BillboardReview: 60, + USBillboard200: 2, + Artist: 'Naomí Yepes', + Songs: [{ + Number: 1, + Title: 'My dream', + Released: new Date('13 Jan 2017'), + Genre: 'Dance-pop EDM', + Album: 'The dragon journey' + }, + { + Number: 2, + Title: 'My passion', + Released: new Date('23 Sep 2017'), + Genre: 'Crunk reggaeton', + Album: 'The dragon journey' + }, + { + Number: 3, + Title: 'What is love', + Released: new Date('28 Nov 2018'), + Genre: 'Dance-pop R&B', + Album: 'The dragon journey' + }, + { + Number: 4, + Title: 'Negative', + Released: new Date('01 Dec 2018'), + Genre: 'Dance-pop EDM', + Album: 'The dragon journey' + }] + }, + { + Album: 'Organic me', + LaunchDate: new Date('August 17, 2018'), + BillboardReview: 82, + USBillboard200: 1, + Artist: 'Naomí Yepes', + Songs: [{ + Number: 1, + Title: 'I Love', + Released: new Date('11 May 2019'), + Genre: 'Crunk reggaeton', + Album: 'Organic me' + }, + { + Number: 2, + Title: 'Early Morning Compass', + Released: new Date('15 Jan 2020'), + Genre: 'mystical parody-bap ', + Album: 'Organic me' + }, + { + Number: 3, + Title: 'Key Fields Forever', + Released: new Date('2 Jan 2020'), + Genre: 'Dance-pop EDM', + Album: 'Organic me' + }, + { + Number: 4, + Title: 'Stand by Your Goblins', + Released: new Date('20 Nov 2019'), + Genre: '*', + Album: 'Organic me' + }, + { + Number: 5, + Title: 'Mad to Walk', + Released: new Date('12 May 2019'), + Genre: 'Electro house Electropop', + Album: 'Organic me' + }, + { + Number: 6, + Title: 'Alice\'s Waiting', + Released: new Date('28 Jan 2020'), + Genre: 'R&B', + Album: 'Organic me' + }, + { + Number: 7, + Title: 'We Shall Kiss', + Released: new Date('30 Oct 2019'), + Genre: '*', + Album: 'Organic me' + }, + { + Number: 8, + Title: 'Behind Single Ants', + Released: new Date('2 Oct 2019'), + Genre: '*', + Album: 'Organic me' + }, + { + Number: 9, + Title: 'Soap Autopsy', + Released: new Date('8 Aug 2019'), + Genre: 'Synth-pop R&B', + Album: 'Organic me' + }, + { + Number: 10, + Title: 'Have You Met Rich?', + Released: new Date('1 Jul 2019'), + Genre: 'ethno-tunes', + Album: 'Organic me' + }, + { + Number: 11, + Title: 'Livin\' on a Banana', + Released: new Date('22 Nov 2019'), + Genre: 'Crunk reggaeton', + Album: 'Organic me' + }] + }, + { + Album: 'Curiosity', + LaunchDate: new Date('December 7, 2019'), + BillboardReview: 75, + USBillboard200: 12, + Artist: 'Naomí Yepes', + Songs: [{ + Number: 1, + Title: 'Goals', + Released: new Date('07 Dec 2019'), + Genre: '*', + Album: 'Curiosity' + }, + { + Number: 2, + Title: 'Explorer', + Released: new Date('08 Dec 2019'), + Genre: 'Crunk reggaeton', + Album: 'Curiosity' + }, + { + Number: 3, + Title: 'I need to know', + Released: new Date('09 Dec 2019'), + Genre: 'Dance-pop R&B', + Album: 'Curiosity' + }, + { + Number: 4, + Title: 'Finding my purpose', + Released: new Date('10 Dec 2019'), + Genre: 'Heavy metal', + Album: 'Curiosity' + }, + { + Number: 5, + Title: 'Faster than the speed of love', + Released: new Date('21 Dec 2019'), + Genre: 'Dance-pop EDM', + Album: 'Curiosity' + }, + { + Number: 6, + Title: 'I like it', + Released: new Date('01 Jan 2020'), + Genre: 'Dance-pop EDM', + Album: 'Curiosity' + }] + } + ] + }, + { + ID: 1, + Artist: 'Babila Ebwélé', + // Photo: 'assets/images/hgrid/babila.jpg', + Debut: 2009, + GrammyNominations: 0, + GrammyAwards: 11, + HasGrammyAward: true, + Tours: [ + { + Tour: 'The last straw', + StartedOn: 'May 09', + Location: 'Europe, Asia', + Headliner: 'NO', + TouredBy: 'Babila Ebwélé' + }, + { + Tour: 'No foundations', + StartedOn: 'Jun 04', + Location: 'United States, Europe', + Headliner: 'YES', + TouredBy: 'Babila Ebwélé' + }, + { + Tour: 'Crazy eyes', + StartedOn: 'Jun 08', + Location: 'North America', + Headliner: 'NO', + TouredBy: 'Babila Ebwélé' + }, + { + Tour: 'Zero gravity', + StartedOn: 'Apr 19', + Location: 'United States', + Headliner: 'NO', + TouredBy: 'Babila Ebwélé' + }, + { + Tour: 'Battle with myself', + StartedOn: 'Mar 08', + Location: 'North America', + Headliner: 'YES', + TouredBy: 'Babila Ebwélé' + }], + Albums: [ + { + Album: 'Pushing up daisies', + LaunchDate: new Date('May 31, 2000'), + BillboardReview: 86, + USBillboard200: 42, + Artist: 'Babila Ebwélé', + Songs: [{ + Number: 1, + Title: 'Wood Shavings Forever', + Released: new Date('9 Jun 2019'), + Genre: '*', + Album: 'Pushing up daisies' + }, + { + Number: 2, + Title: 'Early Morning Drive', + Released: new Date('20 May 2019'), + Genre: '*', + Album: 'Pushing up daisies' + }, + { + Number: 3, + Title: 'Don\'t Natter', + Released: new Date('10 Jun 2019'), + Genre: 'adult calypso-industrial', + Album: 'Pushing up daisies' + }, + { + Number: 4, + Title: 'Stairway to Balloons', + Released: new Date('18 Jun 2019'), + Genre: 'calypso and mariachi', + Album: 'Pushing up daisies' + }, + { + Number: 5, + Title: 'The Number of your Apple', + Released: new Date('29 Oct 2019'), + Genre: '*', + Album: 'Pushing up daisies' + }, + { + Number: 6, + Title: 'Your Delightful Heart', + Released: new Date('24 Feb 2019'), + Genre: '*', + Album: 'Pushing up daisies' + }, + { + Number: 7, + Title: 'Nice Weather For Balloons', + Released: new Date('1 Aug 2019'), + Genre: 'rap-hop', + Album: 'Pushing up daisies' + }, + { + Number: 8, + Title: 'The Girl From Cornwall', + Released: new Date('4 May 2019'), + Genre: 'enigmatic rock-and-roll', + Album: 'Pushing up daisies' + }, + { + Number: 9, + Title: 'Here Without Jack', + Released: new Date('24 Oct 2019'), + Genre: '*', + Album: 'Pushing up daisies' + }, + { + Number: 10, + Title: 'Born Rancid', + Released: new Date('19 Mar 2019'), + Genre: '*', + Album: 'Pushing up daisies' + }] + }, + { + Album: 'Death\'s dead', + LaunchDate: new Date('June 8, 2016'), + BillboardReview: 85, + USBillboard200: 95, + Artist: 'Babila Ebwélé', + Songs: [{ + Number: 1, + Title: 'Men Sound Better With You', + Released: new Date('20 Oct 2016'), + Genre: 'rap-hop', + Album: 'Death\'s dead' + }, + { + Number: 2, + Title: 'Ghost in My Rod', + Released: new Date('5 Oct 2016'), + Genre: 'enigmatic rock-and-roll', + Album: 'Death\'s dead' + }, + { + Number: 3, + Title: 'Bed of Men', + Released: new Date('14 Nov 2016'), + Genre: 'whimsical comedy-grass ', + Album: 'Death\'s dead' + }, + { + Number: 4, + Title: 'Don\'t Push', + Released: new Date('2 Jan 2017'), + Genre: 'unblack electronic-trip-hop', + Album: 'Death\'s dead' + }, + { + Number: 5, + Title: 'Nice Weather For Men', + Released: new Date('18 Dec 2017'), + Genre: '*', + Album: 'Death\'s dead' + }, + { + Number: 6, + Title: 'Rancid Rhapsody', + Released: new Date('10 Mar 2017'), + Genre: '*', + Album: 'Death\'s dead' + }, + { + Number: 7, + Title: 'Push, Push, Push!', + Released: new Date('21 Feb 2017'), + Genre: '*', + Album: 'Death\'s dead' + }, + { + Number: 8, + Title: 'My Name is Sarah', + Released: new Date('15 Nov 2017'), + Genre: '*', + Album: 'Death\'s dead' + }, + { + Number: 9, + Title: 'The Girl From My Hotel', + Released: new Date('6 Nov 2017'), + Genre: '*', + Album: 'Death\'s dead' + }, + { + Number: 10, + Title: 'Free Box', + Released: new Date('18 Apr 2017'), + Genre: 'splitter-funk', + Album: 'Death\'s dead' + }, + { + Number: 11, + Title: 'Hotel Cardiff', + Released: new Date('30 Dec 2017'), + Genre: 'guilty pleasure ebm', + Album: 'Death\'s dead' + }] + }] + }, + { + ID: 2, + Artist: 'Ahmad Nazeri', + // Photo: 'assets/images/hgrid/ahmad.jpg', + Debut: 2004, + GrammyNominations: 3, + GrammyAwards: 1, + HasGrammyAward: true, + Tours: [], + Albums: [ + { + Album: 'Emergency', + LaunchDate: new Date('March 6, 2004'), + BillboardReview: 98, + USBillboard200: 69, + Artist: 'Ahmad Nazeri', + Songs: [{ + Number: 1, + Title: 'I am machine', + Released: new Date('20 Oct 2004'), + Genre: 'Heavy metal', + Album: 'Emergency' + }, + { + Number: 2, + Title: 'I wish I knew', + Released: new Date('21 Oct 2004'), + Genre: 'rap-hop', + Album: 'Emergency' + }, + { + Number: 3, + Title: 'How I feel', + Released: new Date('22 Oct 2004'), + Genre: 'Heavy metal', + Album: 'Emergency' + }, + { + Number: 4, + Title: 'I am machine', + Released: new Date('30 Oct 2004'), + Genre: 'Heavy metal', + Album: 'Emergency' + }, + { + Number: 5, + Title: 'Monsters under my bed', + Released: new Date('01 Nov 2004'), + Genre: 'rap-hop', + Album: 'Emergency' + }, + { + Number: 6, + Title: 'I know what you want', + Released: new Date('20 Nov 2004'), + Genre: 'rap-hop', + Album: 'Emergency' + }, + { + Number: 7, + Title: 'Lies', + Released: new Date('21 Nov 2004'), + Genre: 'Heavy metal', + Album: 'Emergency' + }, + { + Number: 8, + Title: 'I did it for you', + Released: new Date('22 Nov 2004'), + Genre: 'rap-hop', + Album: 'Emergency' + }] + }, + { + Album: 'Bursting bubbles', + LaunchDate: new Date('April 17, 2006'), + BillboardReview: 69, + USBillboard200: 39, + Artist: 'Ahmad Nazeri', + Songs: [{ + Number: 1, + Title: 'Ghosts', + Released: new Date('20 Apr 2006'), + Genre: 'Hip-hop', + Album: 'Bursting bubbles' + }, + { + Number: 2, + Title: 'What goes around comes around', + Released: new Date('20 Apr 2006'), + Genre: 'Heavy metal', + Album: 'Bursting bubbles' + }, + { + Number: 3, + Title: 'I want nothing', + Released: new Date('21 Apr 2006'), + Genre: 'Heavy metal', + Album: 'Bursting bubbles' + }, + { + Number: 4, + Title: 'Me and you', + Released: new Date('22 Apr 2006'), + Genre: 'Rock', + Album: 'Bursting bubbles' + }] + } + ] + }, + { + ID: 3, + Artist: 'Kimmy McIlmorie', + // Photo: 'assets/images/hgrid/kimmy.jpg', + Debut: 2007, + GrammyNominations: 21, + GrammyAwards: 3, + HasGrammyAward: true, + Albums: [ + { + Album: 'Here we go again', + LaunchDate: new Date('November 18, 2017'), + BillboardReview: 68, + USBillboard200: 1, + Artist: 'Kimmy McIlmorie', + Songs: [{ + Number: 1, + Title: 'Same old love', + Released: new Date('20 Nov 2017'), + Genre: 'Hip-hop', + Album: 'Here we go again' + }, + { + Number: 2, + Title: 'Sick of it', + Released: new Date('20 Nov 2017'), + Genre: 'Hip-hop', + Album: 'Here we go again' + }, + { + Number: 3, + Title: 'No one', + Released: new Date('21 Nov 2017'), + Genre: 'Metal', + Album: 'Here we go again' + }, + { + Number: 4, + Title: 'Circles', + Released: new Date('22 Nov 2017'), + Genre: 'Heavy metal', + Album: 'Here we go again' + }, + { + Number: 5, + Title: 'Coming for you', + Released: new Date('30 Nov 2017'), + Genre: 'Hip-hop', + Album: 'Here we go again' + }] + } + ] + }, + { + ID: 4, + Artist: 'Mar Rueda', + // Photo: 'assets/images/hgrid/mar.jpg', + Debut: 1996, + GrammyNominations: 14, + GrammyAwards: 2, + HasGrammyAward: true, + Albums: [ + { + Album: 'Trouble', + LaunchDate: new Date('November 18, 2017'), + BillboardReview: 65, + USBillboard200: 2, + Artist: 'Mar Rueda', + Songs: [{ + Number: 1, + Title: 'You knew I was trouble', + Released: new Date('20 Nov 2017'), + Genre: 'Pop', + Album: 'Trouble' + }, + { + Number: 2, + Title: 'Cannot live without you', + Released: new Date('20 Nov 2017'), + Genre: 'Pop', + Album: 'Trouble' + }, + { + Number: 3, + Title: 'Lost you', + Released: new Date('21 Nov 2017'), + Genre: 'Metal', + Album: 'Trouble' + }, + { + Number: 4, + Title: 'Happiness starts with you', + Released: new Date('22 Nov 2017'), + Genre: '*', + Album: 'Trouble' + }, + { + Number: 5, + Title: 'I saw it coming', + Released: new Date('30 Dec 2017'), + Genre: 'Hip-hop', + Album: 'Trouble' + }] + } + ] + }, + { + ID: 5, + Artist: 'Izabella Tabakova', + // Photo: 'assets/images/hgrid/izabella.jpg', + Debut: 2017, + GrammyNominations: 7, + GrammyAwards: 11, + HasGrammyAward: true, + Tours: [ + { + Tour: 'Final breath', + StartedOn: 'Jun 13', + Location: 'Europe', + Headliner: 'YES', + TouredBy: 'Izabella Tabakova' + }, + { + Tour: 'Once bitten', + StartedOn: 'Dec 18', + Location: 'Australia, United States', + Headliner: 'NO', + TouredBy: 'Izabella Tabakova' + }, + { + Tour: 'Code word', + StartedOn: 'Sep 19', + Location: 'United States, Europe', + Headliner: 'NO', + TouredBy: 'Izabella Tabakova' + }, + { + Tour: 'Final draft', + StartedOn: 'Sep 17', + Location: 'United States, Europe', + Headliner: 'YES', + TouredBy: 'Izabella Tabakova' + } + ], + Albums: [ + { + Album: 'Once bitten', + LaunchDate: new Date('July 16, 2007'), + BillboardReview: 79, + USBillboard200: 53, + Artist: 'Izabella Tabakova', + Songs: [{ + Number: 1, + Title: 'Whole Lotta Super Cats', + Released: new Date('21 May 2019'), + Genre: '*', + Album: 'Once bitten' + }, + { + Number: 2, + Title: 'Enter Becky', + Released: new Date('16 Jan 2020'), + Genre: '*', + Album: 'Once bitten' + }, + { + Number: 3, + Title: 'Your Cheatin\' Flamingo', + Released: new Date('14 Jan 2020'), + Genre: '*', + Album: 'Once bitten' + }, + { + Number: 4, + Title: 'Mad to Kiss', + Released: new Date('6 Nov 2019'), + Genre: 'Synth-pop R&B', + Album: 'Once bitten' + }, + { + Number: 5, + Title: 'Hotel Prague', + Released: new Date('20 Oct 2019'), + Genre: 'ethno-tunes', + Album: 'Once bitten' + }, + { + Number: 6, + Title: 'Jail on My Mind', + Released: new Date('31 May 2019'), + Genre: 'Crunk reggaeton', + Album: 'Once bitten' + }, + { + Number: 7, + Title: 'Amazing Blues', + Released: new Date('29 May 2019'), + Genre: 'mystical parody-bap ', + Album: 'Once bitten' + }, + { + Number: 8, + Title: 'Goody Two Iron Filings', + Released: new Date('4 Jul 2019'), + Genre: 'Electro house Electropop', + Album: 'Once bitten' + }, + { + Number: 9, + Title: 'I Love in Your Arms', + Released: new Date('7 Jun 2019'), + Genre: 'R&B', + Album: 'Once bitten' + }, + { + Number: 10, + Title: 'Truly Madly Amazing', + Released: new Date('12 Sep 2019'), + Genre: 'ethno-tunes', + Album: 'Once bitten' + } + ] + }, + { + Album: 'Your graciousness', + LaunchDate: new Date('November 17, 2004'), + BillboardReview: 69, + USBillboard200: 30, + Artist: 'Izabella Tabakova', + Songs: [ + { + Number: 1, + Title: 'We Shall Tickle', + Released: new Date('31 Aug 2019'), + Genre: 'old emo-garage ', + Album: 'Your graciousness' + }, + { + Number: 2, + Title: 'Snail Boogie', + Released: new Date('14 Jun 2019'), + Genre: '*', + Album: 'Your graciousness' + }, + { + Number: 3, + Title: 'Amazing Liz', + Released: new Date('15 Oct 2019'), + Genre: '*', + Album: 'Your graciousness' + }, + { + Number: 4, + Title: 'When Sexy Aardvarks Cry', + Released: new Date('1 Oct 2019'), + Genre: 'whimsical comedy-grass ', + Album: 'Your graciousness' + }, + { + Number: 5, + Title: 'Stand By Dave', + Released: new Date('18 Aug 2019'), + Genre: 'unblack electronic-trip-hop', + Album: 'Your graciousness' + }, + { + Number: 6, + Title: 'The Golf Course is Your Land', + Released: new Date('2 Apr 2019'), + Genre: '*', + Album: 'Your graciousness' + }, + { + Number: 7, + Title: 'Where Have All the Men Gone?', + Released: new Date('29 Apr 2019'), + Genre: '*', + Album: 'Your graciousness' + }, + { + Number: 8, + Title: 'Rhythm of the Leg', + Released: new Date('5 Aug 2019'), + Genre: 'ethno-tunes', + Album: 'Your graciousness' + }, + { + Number: 9, + Title: 'Baby, I Need Your Hats', + Released: new Date('5 Dec 2019'), + Genre: 'neuro-tunes', + Album: 'Your graciousness' + }, + { + Number: 10, + Title: 'Stand by Your Cat', + Released: new Date('25 Jul 2019'), + Genre: '*', + Album: 'Your graciousness' + }] + }, + { + Album: 'Dark matters', + LaunchDate: new Date('November 3, 2002'), + BillboardReview: 79, + USBillboard200: 85, + Artist: 'Izabella Tabakova', + Songs: [{ + Number: 1, + Title: 'The Sun', + Released: new Date('31 Oct 2002'), + Genre: 'old emo-garage ', + Album: 'Dark matters' + }, + { + Number: 2, + Title: 'I will survive', + Released: new Date('03 Nov 2002'), + Genre: 'old emo-garage ', + Album: 'Dark matters' + }, + { + Number: 3, + Title: 'Try', + Released: new Date('04 Nov 2002'), + Genre: 'old emo-garage ', + Album: 'Dark matters' + }, + { + Number: 4, + Title: 'Miracle', + Released: new Date('05 Nov 2002'), + Genre: 'old emo-garage ', + Album: 'Dark matters' + }] + } + ] + }, + { + ID: 6, + Artist: 'Nguyễn Diệp Chi', + // Photo: 'assets/images/hgrid/nguyen.jpg', + Debut: 1992, + GrammyNominations: 4, + GrammyAwards: 2, + HasGrammyAward: true, + Albums: [ + { + Album: 'Library of liberty', + LaunchDate: new Date('December 22, 2003'), + BillboardReview: 93, + USBillboard200: 5, + Artist: 'Nguyễn Diệp Chi', + Songs: [{ + Number: 1, + Title: 'Book of love', + Released: new Date('31 Dec 2003'), + Genre: 'Hip-hop', + Album: 'Library of liberty' + }, + { + Number: 2, + Title: 'Commitment', + Released: new Date('01 Jan 2004'), + Genre: 'Hip-hop', + Album: 'Library of liberty' + }, + { + Number: 3, + Title: 'Satisfaction', + Released: new Date('01 Jan 2004'), + Genre: 'Hip-hop', + Album: 'Library of liberty' + }, + { + Number: 4, + Title: 'Obsession', + Released: new Date('01 Jan 2004'), + Genre: 'Hip-hop', + Album: 'Library of liberty' + }, + { + Number: 5, + Title: 'Oblivion', + Released: new Date('02 Jan 2004'), + Genre: 'Hip-hop', + Album: 'Library of liberty' + }, + { + Number: 6, + Title: 'Energy', + Released: new Date('03 Jan 2004'), + Genre: 'Hip-hop', + Album: 'Library of liberty' + }] + } + ] + }, + { + ID: 7, + Artist: 'Eva Lee', + // Photo: 'assets/images/hgrid/eva.jpg', + Debut: 2008, + GrammyNominations: 2, + GrammyAwards: 0, + HasGrammyAward: false, + Albums: [ + { + Album: 'Just a tease', + LaunchDate: new Date('May 3, 2001'), + BillboardReview: 91, + USBillboard200: 29, + Artist: 'Eva Lee', + Songs: [{ + Number: 1, + Title: 'We shall see', + Released: new Date('03 May 2001'), + Genre: 'rap-hop', + Album: 'Just a tease' + }, + { + Number: 2, + Title: 'Hopeless', + Released: new Date('04 May 2001'), + Genre: 'rap-hop', + Album: 'Just a tease' + }, + { + Number: 3, + Title: 'Ignorant', + Released: new Date('04 May 2001'), + Genre: 'rap-hop', + Album: 'Just a tease' + }, + { + Number: 4, + Title: 'Dance', + Released: new Date('05 May 2019'), + Genre: 'Metal', + Album: 'Just a tease' + }, + { + Number: 5, + Title: 'Fire', + Released: new Date('06 May 2019'), + Genre: 'Metal', + Album: 'Just a tease' + }] + } + ] + }, + { + ID: 8, + Artist: 'Siri Jakobsson', + // Photo: 'assets/images/hgrid/siri.jpg', + Debut: 1990, + GrammyNominations: 2, + GrammyAwards: 8, + HasGrammyAward: true, + Tours: [ + { + Tour: 'Basket case', + StartedOn: 'Jan 07', + Location: 'Europe, Asia', + Headliner: 'NO', + TouredBy: 'Siri Jakobsson' + }, + { + Tour: 'The bigger fish', + StartedOn: 'Dec 07', + Location: 'United States, Europe', + Headliner: 'YES', + TouredBy: 'Siri Jakobsson' + }, + { + Tour: 'Missed the boat', + StartedOn: 'Jun 09', + Location: 'Europe, Asia', + Headliner: 'NO', + TouredBy: 'Siri Jakobsson' + }, + { + Tour: 'Equivalent exchange', + StartedOn: 'Feb 06', + Location: 'United States, Europe', + Headliner: 'YES', + TouredBy: 'Siri Jakobsson' + }, + { + Tour: 'Damage control', + StartedOn: 'Oct 11', + Location: 'Australia, United States', + Headliner: 'NO', + TouredBy: 'Siri Jakobsson' + } + ], + Albums: [ + { + Album: 'Under the bus', + LaunchDate: new Date('May 14, 2000'), + BillboardReview: 67, + USBillboard200: 67, + Artist: 'Siri Jakobsson', + Songs: [ + { + Number: 1, + Title: 'Jack Broke My Heart At Tesco\'s', + Released: new Date('19 Jan 2020'), + Genre: '*', + Album: 'Under the bus' + }, + { + Number: 2, + Title: 'Cat Deep, Hats High', + Released: new Date('5 Dec 2019'), + Genre: '*', + Album: 'Under the bus' + }, + { + Number: 3, + Title: 'In Snail We Trust', + Released: new Date('31 May 2019'), + Genre: 'hardcore opera', + Album: 'Under the bus' + }, + { + Number: 4, + Title: 'Liz\'s Waiting', + Released: new Date('22 Jul 2019'), + Genre: 'emotional C-jam ', + Album: 'Under the bus' + }, + { + Number: 5, + Title: 'Lifeless Blues', + Released: new Date('14 Jun 2019'), + Genre: '*', + Album: 'Under the bus' + }, + { + Number: 6, + Title: 'I Spin', + Released: new Date('26 Mar 2019'), + Genre: '*', + Album: 'Under the bus' + }, + { + Number: 7, + Title: 'Ring of Rock', + Released: new Date('12 Dec 2019'), + Genre: '*', + Album: 'Under the bus' + }, + { + Number: 8, + Title: 'Livin\' on a Rock', + Released: new Date('17 Apr 2019'), + Genre: '*', + Album: 'Under the bus' + }, + { + Number: 9, + Title: 'Your Lifeless Heart', + Released: new Date('15 Sep 2019'), + Genre: 'adult calypso-industrial', + Album: 'Under the bus' + }, + { + Number: 10, + Title: 'The High Street on My Mind', + Released: new Date('11 Nov 2019'), + Genre: 'calypso and mariachi', + Album: 'Under the bus' + }, + { + Number: 11, + Title: 'Behind Ugly Curtains', + Released: new Date('8 May 2019'), + Genre: '*', + Album: 'Under the bus' + }, + { + Number: 12, + Title: 'Where Have All the Curtains Gone?', + Released: new Date('28 Jun 2019'), + Genre: '*', + Album: 'Under the bus' + }, + { + Number: 13, + Title: 'Ghost in My Apple', + Released: new Date('14 Dec 2019'), + Genre: '*', + Album: 'Under the bus' + }, + { + Number: 14, + Title: 'I Chatter', + Released: new Date('30 Nov 2019'), + Genre: '*', + Album: 'Under the bus' + } + ] + } + ] + }, + { + ID: 9, + Artist: 'Pablo Cambeiro', + // Photo: 'assets/images/hgrid/pablo.jpg', + Debut: 2011, + GrammyNominations: 5, + GrammyAwards: 0, + HasGrammyAward: false, + Tours: [ + { + Tour: 'Beads', + StartedOn: 'May 11', + Location: 'Worldwide', + Headliner: 'NO', + TouredBy: 'Pablo Cambeiro' + }, + { + Tour: 'Concept art', + StartedOn: 'Dec 18', + Location: 'United States', + Headliner: 'YES', + TouredBy: 'Pablo Cambeiro' + }, + { + Tour: 'Glass shoe', + StartedOn: 'Jan 20', + Location: 'Worldwide', + Headliner: 'YES', + TouredBy: 'Pablo Cambeiro' + }, + { + Tour: 'Pushing buttons', + StartedOn: 'Feb 15', + Location: 'Europe, Asia', + Headliner: 'NO', + TouredBy: 'Pablo Cambeiro' + }, + { + Tour: 'Dark matters', + StartedOn: 'Jan 04', + Location: 'Australia, United States', + Headliner: 'YES', + TouredBy: 'Pablo Cambeiro' + }, + { + Tour: 'Greener grass', + StartedOn: 'Sep 09', + Location: 'United States, Europe', + Headliner: 'NO', + TouredBy: 'Pablo Cambeiro' + }, + { + Tour: 'Apparatus', + StartedOn: 'Nov 16', + Location: 'Europe', + Headliner: 'NO', + TouredBy: 'Pablo Cambeiro' + } + ], + Albums: [ + { + Album: 'Fluke', + LaunchDate: new Date('August 4, 2017'), + BillboardReview: 93, + USBillboard200: 98, + Artist: 'Pablo Cambeiro', + Songs: [{ + Number: 1, + Title: 'Silence', + Released: new Date('25 Aug 2017'), + Genre: 'rap-hop', + Album: 'Fluke' + }, + { + Number: 2, + Title: 'Nothing matters anymore', + Released: new Date('25 Aug 2017'), + Genre: '*', + Album: 'Fluke' + }, + { + Number: 3, + Title: 'Everything wrong with me', + Released: new Date('25 Aug 2017'), + Genre: '*', + Album: 'Fluke' + }] + }, + { + Album: 'Crowd control', + LaunchDate: new Date('August 26, 2003'), + BillboardReview: 68, + USBillboard200: 84, + Artist: 'Pablo Cambeiro', + Songs: [{ + Number: 1, + Title: 'My Bed on My Mind', + Released: new Date('25 Mar 2019'), + Genre: 'ethno-tunes', + Album: 'Crowd control' + }, + { + Number: 2, + Title: 'Bright Blues', + Released: new Date('28 Sep 2019'), + Genre: 'neuro-tunes', + Album: 'Crowd control' + }, + { + Number: 3, + Title: 'Sail, Sail, Sail!', + Released: new Date('5 Mar 2019'), + Genre: '*', + Album: 'Crowd control' + }, + { + Number: 4, + Title: 'Hotel My Bed', + Released: new Date('22 Mar 2019'), + Genre: '*', + Album: 'Crowd control' + }, + { + Number: 5, + Title: 'Gonna Make You Mash', + Released: new Date('18 May 2019'), + Genre: '*', + Album: 'Crowd control' + }, + { + Number: 6, + Title: 'Straight Outta America', + Released: new Date('16 Jan 2020'), + Genre: 'hardcore opera', + Album: 'Crowd control' + }, + { + Number: 7, + Title: 'I Drive', + Released: new Date('23 Feb 2019'), + Genre: 'emotional C-jam ', + Album: 'Crowd control' + }, + { + Number: 8, + Title: 'Like a Teddy', + Released: new Date('31 Aug 2019'), + Genre: '*', + Album: 'Crowd control' + }, + { + Number: 9, + Title: 'Teddy Boogie', + Released: new Date('30 Nov 2019'), + Genre: '*', + Album: 'Crowd control' + }] + }] + }, + { + ID: 10, + Artist: 'Athar Malakooti', + // Photo: 'assets/images/hgrid/athar.jpg', + Debut: 2017, + GrammyNominations: 0, + GrammyAwards: 0, + HasGrammyAward: false, + Albums: [ + { + Album: 'Pushing up daisies', + LaunchDate: new Date('February 24, 2016'), + BillboardReview: 74, + USBillboard200: 77, + Artist: 'Athar Malakooti', + Songs: [{ + Number: 1, + Title: 'Actions', + Released: new Date('25 Feb 2016'), + Genre: 'ethno-tunes', + Album: 'Pushing up daisies' + }, + { + Number: 2, + Title: 'Blinding lights', + Released: new Date('28 Feb 2016'), + Genre: 'neuro-tunes', + Album: 'Pushing up daisies' + }, + { + Number: 3, + Title: 'I want more', + Released: new Date('5 Mar 2016'), + Genre: '*', + Album: 'Pushing up daisies' + }, + { + Number: 4, + Title: 'House by the lake', + Released: new Date('22 Mar 2016'), + Genre: '*', + Album: 'Pushing up daisies' + }] + } + ] + }, + { + ID: 11, + Artist: 'Marti Valencia', + // Photo: 'assets/images/hgrid/marti.jpg', + Debut: 2004, + GrammyNominations: 1, + GrammyAwards: 1, + HasGrammyAward: true, + Tours: [ + { + Tour: 'Cat eat cat world', + StartedOn: 'Sep 00', + Location: 'Worldwide', + Headliner: 'YES', + TouredBy: 'Marti Valencia' + }, + { + Tour: 'Final straw', + StartedOn: 'Sep 06', + Location: 'United States, Europe', + Headliner: 'NO', + TouredBy: 'Marti Valencia' + }], + Albums: [ + { + Album: 'Nemesis', + LaunchDate: new Date('June 30, 2004'), + BillboardReview: 94, + USBillboard200: 9, + Artist: 'Marti Valencia', + Songs: [{ + Number: 1, + Title: 'Love in motion', + Released: new Date('25 Jun 2004'), + Genre: 'ethno-tunes', + Album: 'Nemesis' + }, + { + Number: 2, + Title: 'The picture', + Released: new Date('28 Jun 2004'), + Genre: 'neuro-tunes', + Album: 'Nemesis' + }, + { + Number: 3, + Title: 'Flowers', + Released: new Date('5 Jul 2004'), + Genre: '*', + Album: 'Nemesis' + }, + { + Number: 4, + Title: 'Regret', + Released: new Date('22 Aug 2004'), + Genre: 'Heavy metal', + Album: 'Nemesis' + }] + }, + { + Album: 'First chance', + LaunchDate: new Date('January 7, 2019'), + BillboardReview: 96, + USBillboard200: 19, + Artist: 'Marti Valencia', + Songs: [{ + Number: 1, + Title: 'My Name is Jason', + Released: new Date('12 Jul 2019'), + Genre: '*', + Album: 'First chance' + }, + { + Number: 2, + Title: 'Amazing Andy', + Released: new Date('5 Mar 2019'), + Genre: '*', + Album: 'First chance' + }, + { + Number: 3, + Title: 'The Number of your Knight', + Released: new Date('4 Dec 2019'), + Genre: '*', + Album: 'First chance' + }, + { + Number: 4, + Title: 'I Sail', + Released: new Date('3 Mar 2019'), + Genre: '*', + Album: 'First chance' + }, + { + Number: 5, + Title: 'Goody Two Hands', + Released: new Date('11 Oct 2019'), + Genre: 'Electro house Electropop', + Album: 'First chance' + }, + { + Number: 6, + Title: 'Careful With That Knife', + Released: new Date('18 Dec 2019'), + Genre: 'R&B', + Album: 'First chance' + }, + { + Number: 7, + Title: 'Four Single Ants', + Released: new Date('18 Jan 2020'), + Genre: '*', + Album: 'First chance' + }, + { + Number: 8, + Title: 'Kiss Forever', + Released: new Date('10 Aug 2019'), + Genre: '*', + Album: 'First chance' + }, + { + Number: 9, + Title: 'Rich\'s Waiting', + Released: new Date('15 Mar 2019'), + Genre: 'Synth-pop R&B', + Album: 'First chance' + }, + { + Number: 10, + Title: 'Japan is Your Land', + Released: new Date('7 Mar 2019'), + Genre: 'ethno-tunes', + Album: 'First chance' + }, + { + Number: 11, + Title: 'Pencils in My Banana', + Released: new Date('21 Jun 2019'), + Genre: 'Crunk reggaeton', + Album: 'First chance' + }, + { + Number: 12, + Title: 'I Sail in Your Arms', + Released: new Date('30 Apr 2019'), + Genre: 'Synth-pop R&B', + Album: 'First chance' + }] + }, + { + Album: 'God\'s advocate', + LaunchDate: new Date('April 29, 2007'), + BillboardReview: 66, + USBillboard200: 37, + Artist: 'Marti Valencia', + Songs: [{ + Number: 1, + Title: 'Destiny', + Released: new Date('07 May 2007'), + Genre: '*', + Album: 'God\'s advocate' + }, + { + Number: 2, + Title: 'I am the chosen one', + Released: new Date('08 May 2007'), + Genre: 'Heavy metal', + Album: 'God\'s advocate' + }, + { + Number: 3, + Title: 'New me', + Released: new Date('09 May 2007'), + Genre: 'Dance-pop R&B', + Album: 'God\'s advocate' + }, + { + Number: 4, + Title: 'Miss you', + Released: new Date('10 May 2007'), + Genre: 'Heavy metal', + Album: 'God\'s advocate' + }, + { + Number: 5, + Title: 'Turn back the time', + Released: new Date('21 May 2007'), + Genre: 'Dance-pop EDM', + Album: 'God\'s advocate' + }, + { + Number: 6, + Title: 'Let us have fun', + Released: new Date('01 Jun 2007'), + Genre: 'Dance-pop EDM', + Album: 'God\'s advocate' + }] + } + ] + }, + { + ID: 12, + Artist: 'Alicia Stanger', + // Photo: 'assets/images/hgrid/alicia.jpg', + Debut: 2010, + GrammyNominations: 1, + GrammyAwards: 0, + HasGrammyAward: false, + Albums: [ + { + Album: 'Forever alone', + LaunchDate: new Date('November 3, 2005'), + BillboardReview: 82, + USBillboard200: 7, + Artist: 'Alicia Stanger', + Songs: [{ + Number: 1, + Title: 'Brothers', + Released: new Date('25 Oct 2005'), + Genre: 'Hip-hop', + Album: 'Forever alone' + }, + { + Number: 2, + Title: 'Alone', + Released: new Date('28 Oct 2005'), + Genre: 'Heavy metal', + Album: 'Forever alone' + }, + { + Number: 3, + Title: 'I will go on', + Released: new Date('5 Nov 2005'), + Genre: 'Heavy metal', + Album: 'Forever alone' + }, + { + Number: 4, + Title: 'Horses', + Released: new Date('22 Dec 2005'), + Genre: '*', + Album: 'Forever alone' + }] + } + ] + }, + { + ID: 13, + Artist: 'Peter Taylor', + // Photo: 'assets/images/hgrid/peter.jpg', + Debut: 2005, + GrammyNominations: 0, + GrammyAwards: 2, + HasGrammyAward: true, + Tours: [ + { + Tour: 'Love', + StartedOn: 'Jun 04', + Location: 'Europe, Asia', + Headliner: 'YES', + TouredBy: 'Peter Taylor' + }, + { + Tour: 'Fault of treasures', + StartedOn: 'Oct 13', + Location: 'North America', + Headliner: 'NO', + TouredBy: 'Peter Taylor' + }, + { + Tour: 'For eternity', + StartedOn: 'Mar 05', + Location: 'United States', + Headliner: 'YES', + TouredBy: 'Peter Taylor' + }, + { + Tour: 'Time flies', + StartedOn: 'Jun 03', + Location: 'North America', + Headliner: 'NO', + TouredBy: 'Peter Taylor' + }, + { + Tour: 'Highest difficulty', + StartedOn: 'Nov 01', + Location: 'Worldwide', + Headliner: 'YES', + TouredBy: 'Peter Taylor' + }, + { + Tour: 'Sleeping dogs', + StartedOn: 'May 04', + Location: 'United States, Europe', + Headliner: 'NO', + TouredBy: 'Peter Taylor' + } + ], + Albums: [ + { + Album: 'Decisions decisions', + LaunchDate: new Date('April 10, 2008'), + BillboardReview: 85, + USBillboard200: 35, + Artist: 'Peter Taylor', + Songs: [{ + Number: 1, + Title: 'Now that I am alone', + Released: new Date('25 Apr 2008'), + Genre: '*', + Album: 'Decisions decisions' + }, + { + Number: 2, + Title: 'Hopefully', + Released: new Date('26 Apr 2008'), + Genre: '*', + Album: 'Decisions decisions' + }, + { + Number: 3, + Title: 'Wonderful life', + Released: new Date('5 May 2008'), + Genre: '*', + Album: 'Decisions decisions' + }, + { + Number: 4, + Title: 'Amazing world', + Released: new Date('22 Dec 2008'), + Genre: '*', + Album: 'Decisions decisions' + }] + }, + { + Album: 'Climate changed', + LaunchDate: new Date('June 20, 2015'), + BillboardReview: 66, + USBillboard200: 89, + Artist: 'Peter Taylor', + Songs: [{ + Number: 1, + Title: 'This is how I am now', + Released: new Date('22 Jun 2015'), + Genre: 'Hip-hop', + Album: 'Climate changed' + }, + { + Number: 2, + Title: 'I feel', + Released: new Date('26 Jun 2015'), + Genre: 'rap-hop', + Album: 'Climate changed' + }, + { + Number: 3, + Title: 'Do I want to know', + Released: new Date('5 Jul 2015'), + Genre: 'rap-hop', + Album: 'Climate changed' + }, + { + Number: 4, + Title: 'Natural love', + Released: new Date('22 Jul 2015'), + Genre: '*', + Album: 'Climate changed' + }, + { + Number: 5, + Title: 'I will help', + Released: new Date('22 Jul 2015'), + Genre: '*', + Album: 'Climate changed' + }, + { + Number: 6, + Title: 'No matter what', + Released: new Date('22 Jul 2015'), + Genre: 'hip-hop', + Album: 'Climate changed' + }] + } + ] + } +]; diff --git a/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.html b/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.html new file mode 100644 index 00000000000..b7fe0a8600a --- /dev/null +++ b/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.html @@ -0,0 +1,15 @@ +
+

Sample One

+ + + + + + + + + + +
diff --git a/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.scss b/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.scss new file mode 100644 index 00000000000..493f8cca993 --- /dev/null +++ b/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.scss @@ -0,0 +1,13 @@ +:host::ng-deep { + .activeRow { + background-color: rgb(201, 241, 201); + } +} + +[igxButton="contained"] { + margin: 0.5rem; +} + +h4 { + padding-top: 2rem; +} diff --git a/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.ts b/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.ts new file mode 100644 index 00000000000..5e3eff812c3 --- /dev/null +++ b/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.ts @@ -0,0 +1,54 @@ +import { Component, ViewChild, ChangeDetectorRef, AfterViewInit } from '@angular/core'; +import { + IgxHierarchicalGridComponent, + IGX_HIERARCHICAL_GRID_DIRECTIVES, + FilteringExpressionsTree, + FilteringLogic, + IgxStringFilteringOperand, + IgxDateFilteringOperand +} from 'igniteui-angular'; +import { SINGERS } from './data'; + + +@Component({ + selector: 'app-hierarchical-grid-advanced-filtering-sample', + styleUrls: ['hierarchical-grid-advanced-filtering.sample.scss'], + templateUrl: 'hierarchical-grid-advanced-filtering.sample.html', + imports: [IGX_HIERARCHICAL_GRID_DIRECTIVES] +}) +export class HierarchicalGridAdvancedFilteringSampleComponent implements AfterViewInit { + @ViewChild('hierarchicalGrid', { static: true }) + private hierarchicalGrid: IgxHierarchicalGridComponent; + + public localData = []; + + constructor(private cdr: ChangeDetectorRef) { + this.localData = SINGERS; + } + + public ngAfterViewInit() { + const albumsTree = new FilteringExpressionsTree(FilteringLogic.And, undefined, 'Albums', ['Artist']); + albumsTree.filteringOperands.push({ + fieldName: 'LaunchDate', + condition: IgxDateFilteringOperand.instance().condition('after'), + conditionName: IgxDateFilteringOperand.instance().condition('after').name, + searchVal: new Date(2018, 1, 1) + }); + // const toursTree = new FilteringExpressionsTree(FilteringLogic.And, undefined, 'Tours', ['TouredBy']); + // toursTree.filteringOperands.push({ + // fieldName: 'Headliner', + // condition: IgxStringFilteringOperand.instance().condition('equals'), + // conditionName: IgxStringFilteringOperand.instance().condition('equals').name, + // searchVal: 'YES' + // }); + const artistsTree = new FilteringExpressionsTree(FilteringLogic.And, undefined, 'Artists', ['*']); + artistsTree.filteringOperands.push({ + fieldName: 'Artist', + condition: IgxStringFilteringOperand.instance().condition('inQuery'), + conditionName: IgxStringFilteringOperand.instance().condition('inQuery').name, + searchTree: albumsTree + }); + this.hierarchicalGrid.advancedFilteringExpressionsTree = artistsTree; + this.cdr.detectChanges(); + } +} diff --git a/src/app/hierarchical-grid-remote/hierarchical-grid-remote.sample.html b/src/app/hierarchical-grid-remote/hierarchical-grid-remote.sample.html index 2c2d2059def..eca7380d243 100644 --- a/src/app/hierarchical-grid-remote/hierarchical-grid-remote.sample.html +++ b/src/app/hierarchical-grid-remote/hierarchical-grid-remote.sample.html @@ -1,42 +1,37 @@ - - + - Parent Toolbar - - - + Customers - - - - - - - + + - Child Toolbar - Level 2 - - - + Orders - - - - - - + + + + + - Child Toolbar - Level 3 - - - - + Order Details - - - - + + + + + diff --git a/src/app/hierarchical-grid-remote/hierarchical-grid-remote.sample.ts b/src/app/hierarchical-grid-remote/hierarchical-grid-remote.sample.ts index e3f705c25a4..0c9d4b1c08a 100644 --- a/src/app/hierarchical-grid-remote/hierarchical-grid-remote.sample.ts +++ b/src/app/hierarchical-grid-remote/hierarchical-grid-remote.sample.ts @@ -1,85 +1,144 @@ -import { Component, ViewChild, AfterViewInit } from '@angular/core'; +import { Component, ViewChild, OnInit, ChangeDetectorRef, AfterViewInit } from '@angular/core'; import { IgxRowIslandComponent, IgxHierarchicalGridComponent, IGridCreatedEventArgs, - GridSelectionMode, - IGX_HIERARCHICAL_GRID_DIRECTIVES + IGX_HIERARCHICAL_GRID_DIRECTIVES, + FilteringExpressionsTree, + IgxStringFilteringOperand, + EntityType, + IgxNumberFilteringOperand } from 'igniteui-angular'; -import { RemoteService } from '../shared/remote.service'; +import { HttpClient } from '@angular/common/http'; + +const API_ENDPOINT = 'https://data-northwind.indigo.design'; @Component({ selector: 'app-hierarchical-grid-remote-sample', templateUrl: 'hierarchical-grid-remote.sample.html', styleUrls: ['hierarchical-grid-remote.sample.scss'], - providers: [RemoteService], imports: [IGX_HIERARCHICAL_GRID_DIRECTIVES] }) -export class HierarchicalGridRemoteSampleComponent implements AfterViewInit { - @ViewChild('rowIsland1', { static: true }) - private rowIsland1: IgxRowIslandComponent; - +export class HierarchicalGridRemoteSampleComponent implements OnInit, AfterViewInit { @ViewChild('hGrid', { static: true }) private hGrid: IgxHierarchicalGridComponent; public selectionMode; public remoteData = []; public primaryKeys = [ - { name: 'CustomerID', type: 'string', level: 0 }, - { name: 'OrderID', type: 'number', level: 1 }, - { name: 'EmployeeID', type: 'number', level: 2 }, - { name: 'ProductID', type: 'number', level: 2 } + { name: 'Customers', key: 'customerId' }, + { name: 'Orders', key: 'orderId' }, + { name: 'Details', key: 'orderId' } + ]; + public remoteEntities: EntityType[] = [ + { + name: 'Customers', + fields: [ + { field: 'customerId', dataType: 'string' }, + { field: 'companyName', dataType: 'string' }, + { field: 'contactName', dataType: 'string' }, + { field: 'contactTitle', dataType: 'string' } + ], + childEntities: [ + { + name: 'Orders', + fields: [ + { field: 'customerId', dataType: 'string' }, // first field will be treated as foreign key + { field: 'orderId', dataType: 'number' }, + { field: 'employeeId', dataType: 'number' }, + { field: 'shipVia', dataType: 'string' }, + { field: 'freight', dataType: 'number' } + ], + childEntities: [ + { + name: 'Details', + fields: [ + { field: 'orderId', dataType: 'number' }, // first field will be treated as foreign key + { field: 'productId', dataType: 'number' }, + { field: 'unitPrice', dataType: 'number' }, + { field: 'quantity', dataType: 'number' }, + { field: 'discount', dataType: 'number' } + ] + } + ] + } + ] + } ]; - constructor(private remoteService: RemoteService) { - remoteService.url = 'https://services.odata.org/V4/Northwind/Northwind.svc/'; + constructor(private http: HttpClient, private cdr: ChangeDetectorRef) {} - this.remoteService.urlBuilder = (dataState) => this.buildUrl(dataState); - this.selectionMode = GridSelectionMode.none; - } + public ngOnInit() { + const ordersTree = new FilteringExpressionsTree(0, undefined, 'Orders', ['customerId']); + ordersTree.filteringOperands.push({ + fieldName: 'freight', + ignoreCase: false, + condition: IgxNumberFilteringOperand.instance().condition('greaterThanOrEqualTo'), + conditionName: IgxNumberFilteringOperand.instance().condition('greaterThanOrEqualTo').name, + searchVal: '500' + }); - public buildUrl(dataState) { - let qS = ''; - if (dataState) { - qS += `${dataState.key}?`; + const customersTree = new FilteringExpressionsTree(0, undefined, 'Customers', ['customerId', 'companyName', 'contactName', 'contactTitle']); + customersTree.filteringOperands.push({ + fieldName: 'customerId', + condition: IgxStringFilteringOperand.instance().condition('inQuery'), + conditionName: IgxStringFilteringOperand.instance().condition('inQuery').name, + ignoreCase: false, + searchTree: ordersTree + }); + // customersTree.filteringOperands.push({ + // fieldName: 'customerId', + // ignoreCase: false, + // conditionName: IgxStringFilteringOperand.instance().condition('startsWith').name, + // searchVal: 'A' + // }); + this.hGrid.advancedFilteringExpressionsTree = customersTree; + } - const level = dataState.level; - if (level > 0) { - const parentKey = this.primaryKeys.find((key) => key.level === level - 1); - const parentID = typeof dataState.parentID !== 'object' ? dataState.parentID : dataState.parentID[parentKey.name]; + public ngAfterViewInit() { + this.advancedFilteringExprTreeChange(); + } - if (parentKey.type === 'string') { - qS += `$filter=${parentKey.name} eq '${parentID}'`; - } else { - qS += `$filter=${parentKey.name} eq ${parentID}`; - } - } + public advancedFilteringExprTreeChange() { + let tree = this.hGrid.advancedFilteringExpressionsTree; + if (!tree) { + tree = new FilteringExpressionsTree(0, undefined, this.remoteEntities[0].name, this.remoteEntities[0].fields.map(f => f.field)); } - return `${this.remoteService.url}${qS}`; - } - public ngAfterViewInit() { - this.remoteService.getData({ parentID: null, level: 0, key: 'Customers' }, (data) => { - this.remoteData = data['value']; + console.log(tree); + + this.hGrid.isLoading = true; + this.http.post(`${API_ENDPOINT}/QueryBuilder/ExecuteQuery`, tree).subscribe(data =>{ + console.log('data', data); + this.remoteData = Object.values(data)[0]; this.hGrid.isLoading = false; + this.cdr.detectChanges(); + this.calculateColsInView(); }); } - public setterChange() { - this.rowIsland1.rowSelection = this.rowIsland1.rowSelection === GridSelectionMode.multiple - ? GridSelectionMode.none : GridSelectionMode.multiple; - } - - public setterBindingChange() { - this.selectionMode = this.selectionMode === GridSelectionMode.none ? GridSelectionMode.multiple : GridSelectionMode.none; + private calculateColsInView() { + if (this.hGrid.advancedFilteringExpressionsTree) { + this.hGrid.columns.forEach(column => + column.hidden = !this.hGrid.advancedFilteringExpressionsTree.returnFields.includes(column.field)); + } } public gridCreated(event: IGridCreatedEventArgs, rowIsland: IgxRowIslandComponent) { event.grid.isLoading = true; - this.remoteService.getData({ parentID: event.parentID, level: rowIsland.level, key: rowIsland.key }, (data) => { - event.grid.data = data['value']; + const url = this.buildUrl(event, rowIsland); + this.http.get(url).subscribe(data => { + console.log('data', data); + event.grid.data = Object.values(data); event.grid.isLoading = false; - event.grid.cdr.detectChanges(); + this.cdr.detectChanges(); }); } + + private buildUrl(event: IGridCreatedEventArgs, rowIsland: IgxRowIslandComponent) { + const rowIslandKey = this.primaryKeys.find(key => key.name === rowIsland.key).name; + const parentKey = (event.grid.parent as any).key ?? event.grid.parent.advancedFilteringExpressionsTree.entity; + const url = `${API_ENDPOINT}/${parentKey}/${event.parentID}/${rowIslandKey}`; + return url; + } }