diff --git a/projects/igniteui-angular/src/lib/data-operations/data-util.ts b/projects/igniteui-angular/src/lib/data-operations/data-util.ts index 8b00f88fcf3..4827928e04f 100644 --- a/projects/igniteui-angular/src/lib/data-operations/data-util.ts +++ b/projects/igniteui-angular/src/lib/data-operations/data-util.ts @@ -5,7 +5,7 @@ import { IPagingState, PagingError } from './paging-state.interface'; import { IGroupByKey } from './groupby-expand-state.interface'; import { IGroupByRecord } from './groupby-record.interface'; import { IGroupingState } from './groupby-state.interface'; -import { mergeObjects } from '../core/utils'; +import { cloneArray, mergeObjects } from '../core/utils'; import { Transaction, TransactionType, HierarchicalTransaction } from '../services/transaction/transaction'; import { getHierarchy, isHierarchyMatch } from './operations'; import { ColumnType, GridType } from '../grids/common/grid.interface'; @@ -21,6 +21,8 @@ import { import { DefaultDataCloneStrategy, IDataCloneStrategy } from '../data-operations/data-clone-strategy'; import { IGroupingExpression } from './grouping-expression.interface'; import { DefaultMergeStrategy, IGridMergeStrategy } from './merge-strategy'; +import { IFilteringExpressionsTree } from './filtering-expressions-tree'; +import { FilteringStrategy, FilterUtil } from './filtering-strategy'; /** * @hidden @@ -278,6 +280,15 @@ export class DataUtil { return value; } + public static filterDataByExpressions(data: any[], expressionsTree: IFilteringExpressionsTree, grid: GridType): any { + if (expressionsTree.filteringOperands.length) { + const state = { expressionsTree, strategy: FilteringStrategy.instance() }; + data = FilterUtil.filter(cloneArray(data), state, grid); + } + + return data; + } + private static findParentFromPath(data: any[], primaryKey: any, childDataKey: any, path: any[]): any { let collection: any[] = data; let result: any; 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 6a5f0c932fa..c21291b4b3b 100644 --- a/projects/igniteui-angular/src/lib/data-operations/filtering-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/filtering-strategy.ts @@ -2,12 +2,13 @@ import { FilteringLogic, type IFilteringExpression } from './filtering-expressio import { FilteringExpressionsTree, type IFilteringExpressionsTree } from './filtering-expressions-tree'; import { resolveNestedPath, parseDate, formatDate, formatCurrency, columnFieldPath } from '../core/utils'; import type { ColumnType, EntityType, GridType } from '../grids/common/grid.interface'; -import { GridColumnDataType } from './data-util'; +import { DataUtil, GridColumnDataType } from './data-util'; import { SortingDirection } from './sorting-strategy'; import { formatNumber, formatPercent, getLocaleCurrencyCode } from '@angular/common'; import type { IFilteringState } from './filtering-state.interface'; import { isTree } from './expressions-tree-util'; import type { IgxHierarchicalGridComponent } from '../grids/hierarchical-grid/hierarchical-grid.component'; +import { IgxSorting } from '../grids/common/strategy'; const DateType = 'date'; const DateTimeType = 'dateTime'; @@ -132,27 +133,35 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy { public getFilterItems(column: ColumnType, tree: IFilteringExpressionsTree): Promise { const applyFormatter = column.formatter && this.shouldFormatFilterValues(column); - let data = column.grid.gridAPI.filterDataByExpressions(tree); - data = column.grid.gridAPI.sortDataByExpressions(data, - [{ fieldName: column.field, dir: SortingDirection.Asc, ignoreCase: column.sortingIgnoreCase }]); - + const data = this.getFilteredData(column, tree); const pathParts = columnFieldPath(column.field) - let filterItems: IgxFilterItem[] = data.map(record => { - const value = applyFormatter ? - column.formatter(resolveNestedPath(record, pathParts), record) : - resolveNestedPath(record, pathParts); - - return { - value, - label: this.getFilterItemLabel(column, value, !applyFormatter, record) - }; - }); - filterItems = this.getUniqueFilterItems(column, filterItems); + const seenFormattedFilterItems = new Map() + + for (let i = 0; i < data.length; ++i) { + const record = data[i] + const rawValue = resolveNestedPath(record, pathParts); + const formattedValue = applyFormatter ? column.formatter(rawValue, record) : rawValue; + const { key, finalValue } = this.getFilterItemKeyValue(formattedValue, column); + // Deduplicate by normalized key + if (!seenFormattedFilterItems.has(key)) { + const label = this.getFilterItemLabel(column, finalValue, !applyFormatter, record); + seenFormattedFilterItems.set(key, { value: finalValue, label }); + } + } + + let filterItems: IgxFilterItem[] = Array.from(seenFormattedFilterItems.values()); + + filterItems = DataUtil.sort(filterItems, + [{ fieldName: 'value', dir: SortingDirection.Asc, ignoreCase: column.sortingIgnoreCase }], new IgxSorting()) return Promise.resolve(filterItems); } + protected getFilteredData(column: ColumnType, tree: IFilteringExpressionsTree) { + return column.grid.gridAPI.filterDataByExpressions(tree); + } + protected getFilterItemLabel(column: ColumnType, value: any, applyFormatter: boolean, data: any) { if (column.formatter) { if (applyFormatter) { @@ -180,30 +189,33 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy { } } - protected getUniqueFilterItems(column: ColumnType, filterItems: IgxFilterItem[]) { - const filteredUniqueValues = filterItems.reduce((map, item) => { - let key = item.value; - - if (column.dataType === GridColumnDataType.String && column.filteringIgnoreCase) { - key = key?.toString().toLowerCase(); - } else if (column.dataType === GridColumnDataType.DateTime) { - key = item.value?.toString(); - item.value = key ? new Date(key) : key; - } else if (column.dataType === GridColumnDataType.Time) { - const date = key ? new Date(key) : key; - key = date ? new Date().setHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()) : key; - item.value = key ? new Date(key) : key; - } else if (column.dataType === GridColumnDataType.Date) { - const date = key ? new Date(key) : key; - key = date ? new Date(date.getFullYear(), date.getMonth(), date.getDate()).toISOString() : key; - item.value = date; - } - - return map.has(key) ? map : map.set(key, item) - }, new Map()); - const uniqueValues = Array.from(filteredUniqueValues.values()); - - return uniqueValues; + protected getFilterItemKeyValue(value: any, column: ColumnType) { + let key: any = value; + let finalValue = value; + if (column.dataType === GridColumnDataType.String && column.filteringIgnoreCase) { + key = key?.toString().toLowerCase(); + } else if (column.dataType === GridColumnDataType.DateTime) { + key = value?.toString(); + finalValue = key ? new Date(key) : key; + } else if (column.dataType === GridColumnDataType.Time) { + const date = key ? new Date(key) : key; + key = date + ? new Date().setHours( + date.getHours(), + date.getMinutes(), + date.getSeconds(), + date.getMilliseconds() + ) + : date; + finalValue = key ? new Date(key) : key; + } else if (column.dataType === GridColumnDataType.Date) { + const date = key ? new Date(key) : key; + key = date + ? new Date(date.getFullYear(), date.getMonth(), date.getDate()).toISOString() + : date; + finalValue = date; + } + return { key, finalValue }; } protected shouldFormatFilterValues(_column: ColumnType): boolean { diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/base-filtering.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/base-filtering.component.ts index 73980ec8c8c..97a98e67e4d 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/base-filtering.component.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/base-filtering.component.ts @@ -23,6 +23,7 @@ export abstract class BaseFilteringComponent { public abstract columnChange: EventEmitter; public abstract sortingChanged: EventEmitter; public abstract listDataLoaded: EventEmitter; + public abstract filterCleared: EventEmitter; constructor( protected cdr: ChangeDetectorRef, diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-clear-filters.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-clear-filters.component.ts index 65449e4ad25..3342e5c8bd5 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-clear-filters.component.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-clear-filters.component.ts @@ -34,6 +34,7 @@ export class IgxExcelStyleClearFiltersComponent { */ public clearFilter() { this.esf.grid.filteringService.clearFilter(this.esf.column.field); + this.esf.filterCleared.emit(); this.selectAllFilterItems(); } diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-filtering.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-filtering.component.ts index fc77989a402..856f4e7e3ac 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-filtering.component.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-filtering.component.ts @@ -128,6 +128,12 @@ export class IgxGridExcelStyleFilteringComponent extends BaseFilteringComponent @Output() public listDataLoaded = new EventEmitter(); + /** + * @hidden @internal + */ + @Output() + public filterCleared = new EventEmitter(); + @ViewChild('mainDropdown', { read: ElementRef }) public mainDropdown: ElementRef; @@ -160,29 +166,13 @@ export class IgxGridExcelStyleFilteringComponent extends BaseFilteringComponent */ @Input() public set column(value: ColumnType) { - this._column = value; - this.listData = new Array(); - this.columnChange.emit(this._column); - - this.subscriptions?.unsubscribe(); - - if (this._column) { - this.grid.filteringService.registerSVGIcons(); - this.init(); - this.sortingChanged.emit(); - - this.subscriptions = this.grid.columnPin.subscribe(() => { - requestAnimationFrame(() => { - if (!(this.cdr as ViewRef).destroyed) { - this.cdr.detectChanges(); - } - }); - }); - - this.subscriptions.add(this.grid.columnVisibilityChanged.subscribe(() => this.detectChanges())); - this.subscriptions.add(this.grid.sortingExpressionsChange.subscribe(() => this.sortingChanged.emit())); - this.subscriptions.add(this.grid.filteringExpressionsTreeChange.subscribe(() => this.init())); - this.subscriptions.add(this.grid.columnMovingEnd.subscribe(() => this.cdr.markForCheck())); + if (value) { + this._column = value; + this.columnChange.emit(this._column); + if (this.inline) { + // In case external filtering + this.populateData(); + } } } @@ -326,6 +316,16 @@ export class IgxGridExcelStyleFilteringComponent extends BaseFilteringComponent this.inline = false; this.column = column; this.overlayService = overlayService; + + } + + /** + * @hidden @internal + */ + public populateData() { + if (this.column) { + this.afterColumnChange(); + } if (this._originalDisplay) { this.element.nativeElement.style.display = this._originalDisplay; } @@ -421,6 +421,34 @@ export class IgxGridExcelStyleFilteringComponent extends BaseFilteringComponent return this.computedStyles?.getPropertyValue('--component-size'); } + protected afterColumnChange() { + this.listData = new Array(); + this.subscriptions?.unsubscribe(); + + if (this._column) { + this.grid.filteringService.registerSVGIcons(); + this.init(); + this.sortingChanged.emit(); + + this.subscriptions = this.grid.columnPin.subscribe(() => { + requestAnimationFrame(() => { + if (!(this.cdr as ViewRef).destroyed) { + this.cdr.detectChanges(); + } + }); + }); + + this.subscriptions.add(this.grid.columnVisibilityChanged.subscribe(() => this.detectChanges())); + this.subscriptions.add(this.grid.sortingExpressionsChange.subscribe(() => this.sortingChanged.emit())); + this.subscriptions.add(this.grid.filteringExpressionsTreeChange.subscribe(() => { + this.expressionsList = new Array(); + generateExpressionsList(this.column.filteringExpressionsTree, this.grid.filteringLogic, this.expressionsList); + this.cdr.detectChanges(); + })); + this.subscriptions.add(this.grid.columnMovingEnd.subscribe(() => this.cdr.markForCheck())); + } + } + private init() { this.expressionsList = new Array(); generateExpressionsList(this.column.filteringExpressionsTree, this.grid.filteringLogic, this.expressionsList); @@ -510,12 +538,6 @@ export class IgxGridExcelStyleFilteringComponent extends BaseFilteringComponent private renderValues() { this.filterValues = this.generateFilterValues(); this.generateListData(); - this.expressionsList.forEach(expr => { - if (this.column.dataType === GridColumnDataType.String && this.column.filteringIgnoreCase - && expr.expression.searchVal && expr.expression.searchVal instanceof Set) { - this.modifyExpression(expr); - } - }); } private generateFilterValues() { @@ -546,16 +568,6 @@ export class IgxGridExcelStyleFilteringComponent extends BaseFilteringComponent return filterValues; } - private modifyExpression(expr: ExpressionUI) { - const lowerCaseFilterValues = new Set(Array.from(expr.expression.searchVal).map((value: string) => value.toLowerCase())); - - this.grid.data.forEach(item => { - if (typeof item[this.column.field] === "string" && lowerCaseFilterValues.has(item[this.column.field]?.toLowerCase())) { - expr.expression.searchVal.add(item[this.column.field]); - } - }); - } - private generateListData() { this.listData = new Array(); const shouldUpdateSelection = this.areExpressionsSelectable(); diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-search.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-search.component.ts index d366da1a43c..4e8dd6d47a3 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-search.component.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-search.component.ts @@ -76,7 +76,7 @@ export class IgxExcelStyleSearchComponent implements AfterViewInit, OnDestroy { @ViewChild('input', { read: IgxInputDirective, static: true }) public searchInput: IgxInputDirective; - @ViewChild('cancelButton', {read: IgxButtonDirective, static: true }) + @ViewChild('cancelButton', { read: IgxButtonDirective, static: true }) protected cancelButton: IgxButtonDirective; /** @@ -203,7 +203,7 @@ export class IgxExcelStyleSearchComponent implements AfterViewInit, OnDestroy { protected activeDescendant = ''; private _id = `igx-excel-style-search-${NEXT_ID++}`; - private _isLoading; + private _isLoading = true; private _addToCurrentFilterItem: FilterListItem; private _selectAllItem: FilterListItem; private _hierarchicalSelectedItems: FilterListItem[]; @@ -251,6 +251,10 @@ export class IgxExcelStyleSearchComponent implements AfterViewInit, OnDestroy { this.searchInput.nativeElement.focus(); }); }); + + esf.filterCleared.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.clearInput(); + }); } public ngAfterViewInit() { @@ -396,7 +400,7 @@ export class IgxExcelStyleSearchComponent implements AfterViewInit, OnDestroy { return `${this.id}-item-${index}`; } - protected setActiveDescendant() : void { + protected setActiveDescendant(): void { this.activeDescendant = this.focusedItem?.id || ''; } @@ -585,20 +589,38 @@ export class IgxExcelStyleSearchComponent implements AfterViewInit, OnDestroy { blanksItem = selectedItems[blanksItemIndex]; selectedItems.splice(blanksItemIndex, 1); } + let searchVal; + switch (this.esf.column.dataType) { + case GridColumnDataType.Date: + searchVal = new Set(selectedItems.map(d => d.value.toDateString())); + break; + case GridColumnDataType.DateTime: + searchVal = new Set(selectedItems.map(d => d.value.toISOString())); + break; + case GridColumnDataType.Time: + searchVal = new Set(selectedItems.map(e => e.value.toLocaleTimeString())); + break; + case GridColumnDataType.String: + if (this.esf.column.filteringIgnoreCase) { + const selectedValues = new Set(selectedItems.map(item => item.value.toLowerCase())); + searchVal = new Set(); + + this.esf.grid.data.forEach(item => { + if (typeof item[this.esf.column.field] === "string" && selectedValues.has(item[this.esf.column.field]?.toLowerCase())) { + searchVal.add(item[this.esf.column.field]); + } + }); + break; + } + default: + searchVal = new Set(selectedItems.map(e => e.value)) + } filterTree.filteringOperands.push({ condition: this.createCondition('in'), conditionName: 'in', fieldName: this.esf.column.field, ignoreCase: this.esf.column.filteringIgnoreCase, - searchVal: new Set( - this.esf.column.dataType === GridColumnDataType.Date ? - selectedItems.map(d => d.value.toDateString()) : - this.esf.column.dataType === GridColumnDataType.DateTime ? - selectedItems.map(d => d.value.toISOString()) : - this.esf.column.dataType === GridColumnDataType.Time ? - selectedItems.map(e => e.value.toLocaleTimeString()) : - selectedItems.map(e => e.value) - ) + searchVal }); if (blanksItem) { @@ -628,12 +650,12 @@ export class IgxExcelStyleSearchComponent implements AfterViewInit, OnDestroy { if (event) { const key = event.key.toLowerCase(); const navKeys = ['space', 'spacebar', ' ', - 'arrowup', 'up', 'arrowdown', 'down', 'home', 'end']; - if (navKeys.indexOf(key) === -1) { // If key has appropriate function in DD - return; - } - event.preventDefault(); - event.stopPropagation(); + 'arrowup', 'up', 'arrowdown', 'down', 'home', 'end']; + if (navKeys.indexOf(key) === -1) { // If key has appropriate function in DD + return; + } + event.preventDefault(); + event.stopPropagation(); switch (key) { case 'arrowup': case 'up': @@ -827,9 +849,9 @@ export class IgxExcelStyleSearchComponent implements AfterViewInit, OnDestroy { const direction = index > (this.focusedItem ? this.focusedItem.index : -1) ? Navigate.Down : Navigate.Up; const scrollRequired = this.isIndexOutOfBounds(index, direction); this.focusedItem = { - id: this.getItemId(index), - index: index, - checked: this.virtDir.igxForOf[index].isSelected + id: this.getItemId(index), + index: index, + checked: this.virtDir.igxForOf[index].isSelected }; if (scrollRequired) { this.virtDir.scrollTo(index); diff --git a/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering.service.ts b/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering.service.ts index 717e78acbcf..4faded21a0c 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering.service.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering.service.ts @@ -9,7 +9,7 @@ import { takeUntil, first } from 'rxjs/operators'; import { IForOfState } from '../../directives/for-of/for_of.directive'; import { IFilteringOperation } from '../../data-operations/filtering-condition'; import { IColumnResizeEventArgs, IFilteringEventArgs } from '../common/events'; -import { OverlayCancelableEventArgs, OverlaySettings, VerticalAlignment } from '../../services/overlay/utilities'; +import { OverlayCancelableEventArgs, OverlayEventArgs, OverlaySettings, VerticalAlignment } from '../../services/overlay/utilities'; import { IgxOverlayService } from '../../services/overlay/overlay'; import { useAnimation } from '@angular/animations'; import { AbsoluteScrollStrategy } from '../../services/overlay/scroll/absolute-scroll-strategy'; @@ -86,6 +86,12 @@ export class IgxFilteringService implements OnDestroy { this.lastActiveNode = this.grid.navigation.activeNode; }); + this._overlayService.opened.pipe(first(overlay => overlay.id === id), takeUntil(this.destroy$)).subscribe((event: OverlayEventArgs) => { + if (event.componentRef) { + event.componentRef.instance.populateData(); + } + }); + this._overlayService.closed .pipe( first(overlay => overlay.id === id), 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 1f62ef8aa99..932fdfbf96b 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -4143,6 +4143,7 @@ export abstract class IgxGridBaseDirective implements GridType, options.outlet = this.outlet; if (this.excelStyleFilteringComponent) { this.excelStyleFilteringComponent.initialize(column, this.overlayService); + this.excelStyleFilteringComponent.populateData(); const id = this.overlayService.attach(this.excelStyleFilteringComponent.element, options); this.excelStyleFilteringComponent.overlayComponentId = id; return id; diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.ts b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.ts index 57890443e81..ee74558720d 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.ts @@ -1164,6 +1164,7 @@ export class IgxPivotGridComponent extends IgxGridBaseDirective implements OnIni options.outlet = this.outlet; if (dropdown) { dropdown.initialize(column, this.overlayService); + dropdown.populateData(); if (shouldReatach) { const id = this.overlayService.attach(dropdown.element, options); dropdown.overlayComponentId = id; @@ -2219,6 +2220,7 @@ export class IgxPivotGridComponent extends IgxGridBaseDirective implements OnIni ref.instance.resizable = this.rowDimensionResizing; ref.instance.sortable = dim.sortable === undefined ? true : dim.sortable; ref.instance.width = this.rowDimensionWidth(dim); + ref.instance.filteringIgnoreCase = false; ref.changeDetectorRef.detectChanges(); columns.push(ref.instance); }); diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.filtering.strategy.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.filtering.strategy.ts index ae016c28bdd..e049a86580f 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.filtering.strategy.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.filtering.strategy.ts @@ -89,6 +89,10 @@ export class TreeGridFilteringStrategy extends BaseFilteringStrategy { return Promise.resolve(items); } + protected override getFilteredData(column: ColumnType, tree: IFilteringExpressionsTree) { + return DataUtil.filterDataByExpressions(column.grid.flatData, tree, column.grid); + } + private getHierarchicalFilterItems(records: ITreeGridRecord[], column: ColumnType, parent?: IgxFilterItem): IgxFilterItem[] { const pathParts = columnFieldPath(column.field); return records?.map(record => {