diff --git a/CHANGELOG.md b/CHANGELOG.md index bded9c32b7a..df0150ccf36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,17 @@ All notable changes for each version of this project will be documented in this file. -## 13.2.0 +## 13.2.0 ### New palette A new fluent light and dark palettes that use the default fluent colors - `$light-fluent-palette` and `$dark-fluent-palette`. +### New Features +- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid` + - new *sortingOption* property has been introduced on grid level; This property allows you to set either `single` or `multiple` sorting mode; When single mode is enabled you can sort one column at a time; The default value of the property is `multiple`; + + - **Behavioral Change** - sorting and grouping expressions are now working separately; If grouping/sorting expressions are in a conflict, grouping expressions take precedence. You can read more about that in our official documentation. ## 13.1.0 ### New Features @@ -138,7 +143,7 @@ A new fluent light and dark palettes that use the default fluent colors - `$ligh - `IgxDialog` - Added `focusTrap` input to set whether the Tab key focus is trapped within the dialog when opened. Defaults to `true`. - `IgxProgressBar` - - Exposed new animationDuration input - sets the duration of the progress animation. + - Exposed new animationDuration input - sets the duration of the progress animation. ### General diff --git a/projects/igniteui-angular/src/lib/grids/api.service.ts b/projects/igniteui-angular/src/lib/grids/api.service.ts index 5ae1e983846..6307e39e87c 100644 --- a/projects/igniteui-angular/src/lib/grids/api.service.ts +++ b/projects/igniteui-angular/src/lib/grids/api.service.ts @@ -8,6 +8,7 @@ import { IgxCell, IgxGridCRUDService, IgxEditRow } from './common/crud.service'; import { CellType, ColumnType, GridServiceType, GridType, RowType } from './common/grid.interface'; import { IGridEditEventArgs, IRowToggleEventArgs } from './common/events'; import { IgxColumnMovingService } from './moving/moving.service'; +import { IGroupingExpression } from '../data-operations/grouping-expression.interface'; import { ISortingExpression, SortingDirection } from '../data-operations/sorting-strategy'; import { FilterUtil } from '../data-operations/filtering-strategy'; @@ -221,6 +222,15 @@ export class GridBaseAPIService implements GridServiceType { this.grid.sortingExpressions = sortingState; } + public sort_decoupled(expression: IGroupingExpression): void { + if (expression.dir === SortingDirection.None) { + this.remove_grouping_expression(expression.fieldName); + } + const groupingState = cloneArray((this.grid as any).groupingExpressions); + this.prepare_grouping_expression([groupingState], expression); + (this.grid as any).groupingExpressions = groupingState; + } + public sort_multiple(expressions: ISortingExpression[]): void { const sortingState = cloneArray(this.grid.sortingExpressions); @@ -234,6 +244,17 @@ export class GridBaseAPIService implements GridServiceType { this.grid.sortingExpressions = sortingState; } + public sort_groupBy_multiple(expressions: ISortingExpression[]): void { + const groupingState = cloneArray((this.grid as any).groupingExpressions); + + for (const each of expressions) { + if (each.dir === SortingDirection.None) { + this.remove_grouping_expression(each.fieldName); + } + this.prepare_grouping_expression([groupingState], each); + } + } + public clear_sort(fieldName: string) { const sortingState = this.grid.sortingExpressions; const index = sortingState.findIndex((expr) => expr.fieldName === fieldName); @@ -460,6 +481,43 @@ export class GridBaseAPIService implements GridServiceType { }); } + public prepare_grouping_expression(stateCollections: Array>, expression: IGroupingExpression) { + if (expression.dir === SortingDirection.None) { + stateCollections.forEach(state => { + state.splice(state.findIndex((expr) => expr.fieldName === expression.fieldName), 1); + }); + return; + } + + /** + * We need to make sure the states in each collection with same fields point to the same object reference. + * If the different state collections provided have different sizes we need to get the largest one. + * That way we can get the state reference from the largest one that has the same fieldName as the expression to prepare. + */ + let maxCollection = stateCollections[0]; + for (let i = 1; i < stateCollections.length; i++) { + if (maxCollection.length < stateCollections[i].length) { + maxCollection = stateCollections[i]; + } + } + const maxExpr = maxCollection.find((expr) => expr.fieldName === expression.fieldName); + + stateCollections.forEach(collection => { + const myExpr = collection.find((expr) => expr.fieldName === expression.fieldName); + if (!myExpr && !maxExpr) { + // Expression with this fieldName is missing from the current and the max collection. + collection.push(expression); + } else if (!myExpr && maxExpr) { + // Expression with this fieldName is missing from the current and but the max collection has. + collection.push(maxExpr); + Object.assign(maxExpr, expression); + } else { + // The current collection has the expression so just update it. + Object.assign(myExpr, expression); + } + }); + } + public remove_grouping_expression(_fieldName) { } diff --git a/projects/igniteui-angular/src/lib/grids/columns/interfaces.ts b/projects/igniteui-angular/src/lib/grids/columns/interfaces.ts index 1d22ac220b8..ac2e9bf7012 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/interfaces.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/interfaces.ts @@ -43,3 +43,7 @@ export interface IColumnPipeArgs { */ display?: string; } + +export interface ISortingOptions { + mode: 'single' | 'multiple'; +} diff --git a/projects/igniteui-angular/src/lib/grids/common/events.ts b/projects/igniteui-angular/src/lib/grids/common/events.ts index 962da9c694d..43c7bbeba40 100644 --- a/projects/igniteui-angular/src/lib/grids/common/events.ts +++ b/projects/igniteui-angular/src/lib/grids/common/events.ts @@ -2,6 +2,7 @@ import { IBaseEventArgs, CancelableEventArgs } from '../../core/utils'; import { GridKeydownTargetType } from './enums'; import { CellType, ColumnType, GridType, RowType } from './grid.interface'; import { IFilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree'; +import { IGroupingExpression } from '../../data-operations/grouping-expression.interface'; import { IgxBaseExporter } from '../../services/exporter-common/base-export-service'; import { IgxExporterOptionsBase } from '../../services/exporter-common/exporter-options-base'; import { ISortingExpression } from '../../data-operations/sorting-strategy'; @@ -201,7 +202,8 @@ export interface IActiveNodeChangeEventArgs extends IBaseEventArgs { } export interface ISortingEventArgs extends IBaseEventArgs, CancelableEventArgs { - sortingExpressions: ISortingExpression | Array; + sortingExpressions?: ISortingExpression | Array; + groupingExpressions?: IGroupingExpression | Array; } export interface IFilteringEventArgs extends IBaseEventArgs, CancelableEventArgs { 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 f2014df8fe6..9ec9a6448f3 100644 --- a/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts +++ b/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts @@ -27,7 +27,7 @@ import { ITreeGridRecord } from '../tree-grid/tree-grid.interfaces'; import { State, Transaction, TransactionService } from '../../services/transaction/transaction'; import { GridColumnDataType } from '../../data-operations/data-util'; import { IgxFilteringOperand } from '../../data-operations/filtering-condition'; -import { IColumnPipeArgs, MRLResizeColumnInfo } from '../columns/interfaces'; +import { IColumnPipeArgs, ISortingOptions, MRLResizeColumnInfo } from '../columns/interfaces'; import { IgxSummaryResult } from '../summaries/grid-summary'; import { ISortingExpression, ISortingStrategy, SortingDirection } from '../../data-operations/sorting-strategy'; import { IGridGroupingStrategy, IGridSortingStrategy } from './strategy'; @@ -444,6 +444,7 @@ export interface GridType extends IGridDataBindable { records?: Map; processedExpandedFlatData?: any[] | null; processedRecords?: Map; + treeGroupArea?: any; activeNodeChange: EventEmitter; gridKeydown: EventEmitter; @@ -499,6 +500,7 @@ export interface GridType extends IGridDataBindable { filteringExpressionsTreeChange: EventEmitter; advancedFilteringExpressionsTree: IFilteringExpressionsTree; advancedFilteringExpressionsTreeChange: EventEmitter; + sortingOptions: ISortingOptions; batchEditing: boolean; groupingExpansionState?: IGroupByExpandState[]; 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 24ad523fd88..32c9795cb0e 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -138,6 +138,7 @@ import { IgxPaginatorComponent } from '../paginator/paginator.component'; import { IgxGridHeaderRowComponent } from './headers/grid-header-row.component'; import { IgxGridGroupByAreaComponent } from './grouping/grid-group-by-area.component'; import { IgxFlatTransactionFactory, TRANSACTION_TYPE } from '../services/transaction/transaction-factory.service'; +import { ISortingOptions } from './columns/interfaces'; import { GridSelectionRange, IgxGridTransaction } from './common/types'; import { VerticalAlignment, HorizontalAlignment, PositionSettings, OverlaySettings } from '../services/overlay/utilities'; import { IgxOverlayService } from '../services/overlay/overlay'; @@ -145,7 +146,7 @@ import { ConnectedPositioningStrategy } from '../services/overlay/position/conne import { ContainerPositionStrategy } from '../services/overlay/position/container-position-strategy'; import { AbsoluteScrollStrategy } from '../services/overlay/scroll/absolute-scroll-strategy'; import { Action, StateUpdateEvent, TransactionEventOrigin } from '../services/transaction/transaction'; -import { ISortingExpression, SortingDirection } from '../data-operations/sorting-strategy'; +import { ISortingExpression } from '../data-operations/sorting-strategy'; import { IGridSortingStrategy } from './common/strategy'; import { IgxGridExcelStyleFilteringComponent } from './filtering/excel-style/grid.excel-style-filtering.component'; import { IgxGridHeaderComponent } from './headers/grid-header.component'; @@ -2110,6 +2111,33 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements this._sortingStrategy = value; } + /** + * Gets/Sets the sorting options - single or multiple sorting. + * Accepts an `ISortingOptions` object with any of the `mode` properties. + * + * @example + * ```typescript + * const _sortingOptions: ISortingOptions = { + * mode: 'single' + * } + * ```html + * + * ``` + */ + @Input() + public set sortingOptions(value: ISortingOptions) { + this.clearSort(); + this._sortingOptions = Object.assign(this._sortingOptions, value); + } + + /** + * @hidden + * @internal + */ + public get sortingOptions() { + return this._sortingOptions; + } + /** * Gets/Sets the current selection state. * @@ -2764,6 +2792,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements protected _userOutletDirective: IgxOverlayOutletDirective; protected _transactions: TransactionService; protected _batchEditing = false; + protected _sortingOptions: ISortingOptions = { mode: 'multiple' }; protected _filterStrategy: IFilteringStrategy = new FilteringStrategy(); protected _autoGeneratedCols = []; protected _dataView = []; @@ -4527,14 +4556,15 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements if (expression instanceof Array) { for (const each of expression) { - if (each.dir === SortingDirection.None) { - this.gridAPI.remove_grouping_expression(each.fieldName); - } this.gridAPI.prepare_sorting_expression([sortingState], each); } } else { - if (expression.dir === SortingDirection.None) { - this.gridAPI.remove_grouping_expression(expression.fieldName); + if (this._sortingOptions.mode === 'single') { + this.columns.forEach((col) => { + if (!(col.field === expression.fieldName)) { + this.clearSort(col.field); + } + }); } this.gridAPI.prepare_sorting_expression([sortingState], expression); } diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-api.service.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-api.service.ts index 1217e536f1b..590685ab986 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-api.service.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-api.service.ts @@ -12,18 +12,16 @@ export class IgxGridAPIService extends GridBaseAPIService implements G public groupBy(expression: IGroupingExpression): void { const groupingState = cloneArray(this.grid.groupingExpressions); - const sortingState = cloneArray(this.grid.sortingExpressions); - this.prepare_sorting_expression([sortingState, groupingState], expression); + this.prepare_grouping_expression([groupingState], expression); this.grid.groupingExpressions = groupingState; this.arrange_sorting_expressions(); } public groupBy_multiple(expressions: IGroupingExpression[]): void { const groupingState = cloneArray(this.grid.groupingExpressions); - const sortingState = cloneArray(this.grid.sortingExpressions); for (const each of expressions) { - this.prepare_sorting_expression([sortingState, groupingState], each); + this.prepare_grouping_expression([groupingState], each); } this.grid.groupingExpressions = groupingState; @@ -32,14 +30,11 @@ export class IgxGridAPIService extends GridBaseAPIService implements G public clear_groupby(name?: string | Array) { const groupingState = cloneArray(this.grid.groupingExpressions); - const sortingState = cloneArray(this.grid.sortingExpressions); if (name) { const names = typeof name === 'string' ? [name] : name; const groupedCols = groupingState.filter((state) => names.indexOf(state.fieldName) < 0); - const newSortingExpr = sortingState.filter((state) => names.indexOf(state.fieldName) < 0); this.grid.groupingExpressions = groupedCols; - this.grid.sortingExpressions = newSortingExpr; names.forEach((colName) => { const grExprIndex = groupingState.findIndex((exp) => exp.fieldName === colName); const grpExpandState = this.grid.groupingExpansionState; @@ -56,13 +51,6 @@ export class IgxGridAPIService extends GridBaseAPIService implements G // clear all this.grid.groupingExpressions = []; this.grid.groupingExpansionState = []; - for (const grExpr of groupingState) { - const sortExprIndex = sortingState.findIndex((exp) => exp.fieldName === grExpr.fieldName); - if (sortExprIndex > -1) { - sortingState.splice(sortExprIndex, 1); - } - } - this.grid.sortingExpressions = sortingState; } } @@ -134,19 +122,14 @@ export class IgxGridAPIService extends GridBaseAPIService implements G public arrange_sorting_expressions() { const groupingState = this.grid.groupingExpressions; - this.grid.sortingExpressions.sort((a, b) => { - const groupExprA = groupingState.find((expr) => expr.fieldName === a.fieldName); - const groupExprB = groupingState.find((expr) => expr.fieldName === b.fieldName); - if (groupExprA && groupExprB) { - return groupingState.indexOf(groupExprA) > groupingState.indexOf(groupExprB) ? 1 : -1; - } else if (groupExprA) { - return -1; - } else if (groupExprB) { - return 1; - } else { - return 0; + const sortingState = cloneArray(this.grid.sortingExpressions); + for (const grExpr of groupingState) { + const sortExprIndex = sortingState.findIndex((exp) => exp.fieldName === grExpr.fieldName); + if (sortExprIndex > -1) { + sortingState.splice(sortExprIndex, 1); } - }); + } + this.grid.sortingExpressions = sortingState; } public get_groupBy_record_id(gRow: IGroupByRecord): string { diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html index b02f53aa3ac..501821074f4 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -52,7 +52,7 @@ | gridAddRow:true:pipeTrigger | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true - | gridSort:sortingExpressions:sortStrategy:id:pipeTrigger:true as pinnedData'> + | gridSort:sortingExpressions:groupingExpressions:sortStrategy:id:pipeTrigger:true as pinnedData'>
{ + fixture = TestBed.createComponent(SortByParityComponent); + fixture.detectChanges(); + grid = fixture.componentInstance.grid; + const fieldName = 'Name'; + const fieldLastName = 'LastName'; + const header = GridFunctions.getColumnHeader(fieldName, fixture, grid); + const headerLastName = GridFunctions.getColumnHeader(fieldLastName, fixture, grid); + + grid.sortingOptions = {mode: 'single'}; + fixture.detectChanges(); + + GridFunctions.clickHeaderSortIcon(header); + tick(30); + fixture.detectChanges(); + expect(GridFunctions.getColumnSortingIndex(header)).toBeNull(); + expect(grid.sortingExpressions.length).toBe(1); + + GridFunctions.clickHeaderSortIcon(headerLastName); + tick(30); + fixture.detectChanges(); + + expect(GridFunctions.getColumnSortingIndex(headerLastName)).toBeNull(); + expect(grid.sortingExpressions.length).toBe(1); + })); }); }); diff --git a/projects/igniteui-angular/src/lib/grids/grouping/group-by-area.directive.ts b/projects/igniteui-angular/src/lib/grids/grouping/group-by-area.directive.ts index b675fbf0e3b..116397f3eb0 100644 --- a/projects/igniteui-angular/src/lib/grids/grouping/group-by-area.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grouping/group-by-area.directive.ts @@ -1,194 +1,195 @@ -import { - Directive, - ElementRef, - EventEmitter, - HostBinding, - Input, - Output, - Pipe, - PipeTransform, - QueryList, - TemplateRef, - ViewChildren -} from '@angular/core'; -import { IChipsAreaReorderEventArgs, IgxChipComponent } from '../../chips/public_api'; -import { DisplayDensity } from '../../core/displayDensity'; -import { PlatformUtil } from '../../core/utils'; -import { IGroupingExpression } from '../../data-operations/grouping-expression.interface'; -import { SortingDirection } from '../../data-operations/sorting-strategy'; -import { GridType } from '../common/grid.interface'; -import { IgxColumnMovingDragDirective } from '../moving/moving.drag.directive'; - -/** - * An internal component representing a base group-by drop area. - * - * @hidden @internal - */ - @Directive() -export abstract class IgxGroupByAreaDirective { - /** - * The drop area template if provided by the parent grid. - * Otherwise, uses the default internal one. - */ - @Input() - public dropAreaTemplate: TemplateRef; - - @Input() - public density: DisplayDensity = DisplayDensity.comfortable; - - @HostBinding('class.igx-grid-grouparea') - public defaultClass = true; - - @HostBinding('class.igx-grid-grouparea--cosy') - public get cosyStyle() { - return this.density === 'cosy'; - } - - @HostBinding('class.igx-grid-grouparea--compact') - public get compactStyle() { - return this.density === 'compact'; - } - - /** The parent grid containing the component. */ - @Input() - public grid: GridType; - - /** - * The group-by expressions provided by the parent grid. - */ - @Input() - public get expressions(): IGroupingExpression[] { - return this._expressions; - } - - public set expressions(value: IGroupingExpression[]) { - this._expressions = value; - this.chipExpressions = this._expressions; - this.expressionsChanged(); - this.expressionsChange.emit(this._expressions); - } - - /** - * The default message for the default drop area template. - * Obviously, if another template is provided, this is ignored. - */ - @Input() - public get dropAreaMessage(): string { - return this._dropAreaMessage ?? this.grid.resourceStrings.igx_grid_groupByArea_message; - } - - public set dropAreaMessage(value: string) { - this._dropAreaMessage = value; - } - - @Output() - public expressionsChange = new EventEmitter(); - - @ViewChildren(IgxChipComponent) - public chips: QueryList; - - public chipExpressions: IGroupingExpression[]; - - /** The native DOM element. Used in sizing calculations. */ - public get nativeElement() { - return this.ref.nativeElement; - } - - private _expressions: IGroupingExpression[] = []; - private _dropAreaMessage: string; - - constructor(private ref: ElementRef, protected platform: PlatformUtil) { } - - - public get dropAreaVisible(): boolean { - return (this.grid.columnInDrag && this.grid.columnInDrag.groupable) || - !this.expressions.length; - } - - public handleKeyDown(id: string, event: KeyboardEvent) { - if (this.platform.isActivationKey(event)) { - this.updateSorting(id); - } - } - - public handleClick(id: string) { - if (!this.grid.getColumnByName(id).groupable) { - return; - } - this.updateSorting(id); - } - - public onDragDrop(event) { - const drag: IgxColumnMovingDragDirective = event.detail.owner; - if (drag instanceof IgxColumnMovingDragDirective) { - const column = drag.column; - if (!this.grid.columnList.find(c => c === column)) { - return; - } - - const isGrouped = this.expressions.findIndex((item) => item.fieldName === column.field) !== -1; - if (column.groupable && !isGrouped && !column.columnGroup && !!column.field) { - const groupingExpression = { - fieldName: column.field, - dir: this.grid.sortingExpressions.find(expr => expr.fieldName === column.field)?.dir || SortingDirection.Asc, - ignoreCase: column.sortingIgnoreCase, - strategy: column.sortStrategy, - groupingComparer: column.groupingComparer - }; - - this.groupBy(groupingExpression); - } - } - } - - protected getReorderedExpressions(chipsArray: IgxChipComponent[]) { - const newExpressions = []; - - chipsArray.forEach(chip => { - const expr = this.expressions.find(item => item.fieldName === chip.id); - - // disallow changing order if there are columns with groupable: false - if (!this.grid.getColumnByName(expr.fieldName)?.groupable) { - return; - } - - newExpressions.push(expr); - }); - - return newExpressions; - } - - protected updateSorting(id: string) { - const expr = this.grid.sortingExpressions.find(e => e.fieldName === id); - expr.dir = 3 - expr.dir; - this.grid.sort(expr); - } - - protected expressionsChanged() { - } - - public abstract handleReorder(event: IChipsAreaReorderEventArgs); - - public abstract handleMoveEnd(); - - public abstract groupBy(expression: IGroupingExpression); - - public abstract clearGrouping(name: string); - -} - -/** - * A pipe to circumvent the use of getters/methods just to get some additional - * information from the grouping expression and pass it to the chip representing - * that expression. - * - * @hidden @internal - */ -@Pipe({ name: 'igxGroupByMeta' }) -export class IgxGroupByMetaPipe implements PipeTransform { - - public transform(key: string, grid: GridType) { - const column = grid.getColumnByName(key); - return { groupable: !!column?.groupable, title: column?.header || key }; - } -} +import { + Directive, + ElementRef, + EventEmitter, + HostBinding, + Input, + Output, + Pipe, + PipeTransform, + QueryList, + TemplateRef, + ViewChildren +} from '@angular/core'; +import { IChipsAreaReorderEventArgs, IgxChipComponent } from '../../chips/public_api'; +import { DisplayDensity } from '../../core/displayDensity'; +import { PlatformUtil } from '../../core/utils'; +import { IGroupingExpression } from '../../data-operations/grouping-expression.interface'; +import { SortingDirection } from '../../data-operations/sorting-strategy'; +import { FlatGridType, GridType } from '../common/grid.interface'; +import { IgxColumnMovingDragDirective } from '../moving/moving.drag.directive'; + +/** + * An internal component representing a base group-by drop area. + * + * @hidden @internal + */ + @Directive() +export abstract class IgxGroupByAreaDirective { + /** + * The drop area template if provided by the parent grid. + * Otherwise, uses the default internal one. + */ + @Input() + public dropAreaTemplate: TemplateRef; + + @Input() + public density: DisplayDensity = DisplayDensity.comfortable; + + @HostBinding('class.igx-grid-grouparea') + public defaultClass = true; + + @HostBinding('class.igx-grid-grouparea--cosy') + public get cosyStyle() { + return this.density === 'cosy'; + } + + @HostBinding('class.igx-grid-grouparea--compact') + public get compactStyle() { + return this.density === 'compact'; + } + + /** The parent grid containing the component. */ + @Input() + public grid: FlatGridType | GridType; + + /** + * The group-by expressions provided by the parent grid. + */ + @Input() + public get expressions(): IGroupingExpression[] { + return this._expressions; + } + + public set expressions(value: IGroupingExpression[]) { + this._expressions = value; + this.chipExpressions = this._expressions; + this.expressionsChanged(); + this.expressionsChange.emit(this._expressions); + } + + /** + * The default message for the default drop area template. + * Obviously, if another template is provided, this is ignored. + */ + @Input() + public get dropAreaMessage(): string { + return this._dropAreaMessage ?? this.grid.resourceStrings.igx_grid_groupByArea_message; + } + + public set dropAreaMessage(value: string) { + this._dropAreaMessage = value; + } + + @Output() + public expressionsChange = new EventEmitter(); + + @ViewChildren(IgxChipComponent) + public chips: QueryList; + + public chipExpressions: IGroupingExpression[]; + + /** The native DOM element. Used in sizing calculations. */ + public get nativeElement() { + return this.ref.nativeElement; + } + + private _expressions: IGroupingExpression[] = []; + private _dropAreaMessage: string; + + constructor(private ref: ElementRef, protected platform: PlatformUtil) { } + + + public get dropAreaVisible(): boolean { + return (this.grid.columnInDrag && this.grid.columnInDrag.groupable) || + !this.expressions.length; + } + + public handleKeyDown(id: string, event: KeyboardEvent) { + if (this.platform.isActivationKey(event)) { + this.updateGroupSorting(id); + } + } + + public handleClick(id: string) { + if (!this.grid.getColumnByName(id).groupable) { + return; + } + this.updateGroupSorting(id); + } + + public onDragDrop(event) { + const drag: IgxColumnMovingDragDirective = event.detail.owner; + if (drag instanceof IgxColumnMovingDragDirective) { + const column = drag.column; + if (!this.grid.columnList.find(c => c === column)) { + return; + } + + const isGrouped = this.expressions.findIndex((item) => item.fieldName === column.field) !== -1; + if (column.groupable && !isGrouped && !column.columnGroup && !!column.field) { + const groupingExpression = { + fieldName: column.field, + dir: this.grid.sortingExpressions.find(expr => expr.fieldName === column.field)?.dir || SortingDirection.Asc, + ignoreCase: column.sortingIgnoreCase, + strategy: column.sortStrategy, + groupingComparer: column.groupingComparer + }; + + this.groupBy(groupingExpression); + } + } + } + + protected getReorderedExpressions(chipsArray: IgxChipComponent[]) { + const newExpressions = []; + + chipsArray.forEach(chip => { + const expr = this.expressions.find(item => item.fieldName === chip.id); + + // disallow changing order if there are columns with groupable: false + if (!this.grid.getColumnByName(expr.fieldName)?.groupable) { + return; + } + + newExpressions.push(expr); + }); + + return newExpressions; + } + + protected updateGroupSorting(id: string) { + const expr = this.expressions.find(e => e.fieldName === id); + expr.dir = 3 - expr.dir; + this.grid.pipeTrigger++; + this.grid.notifyChanges(); + } + + protected expressionsChanged() { + } + + public abstract handleReorder(event: IChipsAreaReorderEventArgs); + + public abstract handleMoveEnd(); + + public abstract groupBy(expression: IGroupingExpression); + + public abstract clearGrouping(name: string); + +} + +/** + * A pipe to circumvent the use of getters/methods just to get some additional + * information from the grouping expression and pass it to the chip representing + * that expression. + * + * @hidden @internal + */ +@Pipe({ name: 'igxGroupByMeta' }) +export class IgxGroupByMetaPipe implements PipeTransform { + + public transform(key: string, grid: GridType) { + const column = grid.getColumnByName(key); + return { groupable: !!column?.groupable, title: column?.header || key }; + } +} diff --git a/projects/igniteui-angular/src/lib/grids/grouping/tree-grid-group-by-area.component.ts b/projects/igniteui-angular/src/lib/grids/grouping/tree-grid-group-by-area.component.ts index 4aa02669e24..1752becd1f6 100644 --- a/projects/igniteui-angular/src/lib/grids/grouping/tree-grid-group-by-area.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grouping/tree-grid-group-by-area.component.ts @@ -108,40 +108,9 @@ export class IgxTreeGridGroupByAreaComponent extends IgxGroupByAreaDirective imp } protected expressionsChanged() { - this.updateSortingExpressions(); this.updateColumnsVisibility(); } - private updateSortingExpressions() { - const sortingExpressions = this.grid.sortingExpressions; - let changed = false; - - this.expressions.forEach((expr, index) => { - const sortingIndex = sortingExpressions.findIndex(s => s.fieldName === expr.fieldName); - - if (sortingIndex > -1) { - if (sortingIndex !== index) { - const sortExpr = sortingExpressions.splice(sortingIndex, 1)[0]; - sortExpr.dir = expr.dir; - sortingExpressions.splice(index, 0, sortExpr); - changed = true; - } else if (sortingExpressions[sortingIndex].dir !== expr.dir) { - sortingExpressions[sortingIndex].dir = expr.dir; - changed = true; - } - } else { - const exprCopy = { ...expr }; - sortingExpressions.splice(index, 0, exprCopy); - changed = true; - } - - }); - - if (changed) { - this.grid.sortingExpressions = [...sortingExpressions]; - } - } - private updateColumnsVisibility() { if (this.groupingDiffer && this.grid.columnList && !this.grid.hasColumnLayouts) { const changes = this.groupingDiffer.diff(this.expressions); diff --git a/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.html b/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.html index f660f47979b..4f8bc338a18 100644 --- a/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.html +++ b/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.html @@ -17,8 +17,9 @@
- -
+
@@ -30,4 +31,4 @@
-
\ No newline at end of file + diff --git a/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts b/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts index a186fbd52d0..1365d59ce3a 100644 --- a/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts +++ b/projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts @@ -149,6 +149,16 @@ export class IgxGridHeaderComponent implements DoCheck, OnDestroy { return this.defaultSortHeaderIconTemplate; } } + /** + * @hidden + */ + public get disabled() { + const groupArea = this.grid.groupArea || this.grid.treeGroupArea; + if (groupArea?.expressions && groupArea.expressions.length && groupArea.expressions.map(g => g.fieldName).includes(this.column.field)) { + return true; + } + return false; + } public get sorted() { return this.sortDirection !== SortingDirection.None; diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html index da9c9e45052..a64cb60a382 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html @@ -34,7 +34,7 @@ | gridAddRow:true:pipeTrigger | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true - | gridSort:sortingExpressions:sortStrategy:id:pipeTrigger:true as pinnedData"> + | gridSort:sortingExpressions:[]:sortStrategy:id:pipeTrigger:true as pinnedData">
@@ -51,7 +51,7 @@ | gridTransaction:id:pipeTrigger | visibleColumns:hasVisibleColumns | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger - | gridSort:sortingExpressions:sortStrategy:id:pipeTrigger + | gridSort:sortingExpressions:[]:sortStrategy:id:pipeTrigger | gridHierarchicalPaging:paginator?.page:paginator?.perPage:id:pipeTrigger | gridHierarchical:expansionStates:id:primaryKey:childLayoutKeys:pipeTrigger | gridAddRow:false:pipeTrigger diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-grouping.spec.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-grouping.spec.ts index 937c7733171..899db2ebf15 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-grouping.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-grouping.spec.ts @@ -120,7 +120,7 @@ describe('IgxTreeGrid', () => { tick(); rows = TreeGridFunctions.getAllRows(fix); - expect(rows.length).toBe(20); + expect(rows.length).toBe(treeGrid.rowList.length); })); it('shows a new group chip when adding a grouping expression', fakeAsync(() => { diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html index 0387fc15233..b2fe756d3db 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html @@ -34,7 +34,7 @@ | treeGridAddRow:true:pipeTrigger | gridRowPinning:id:true:pipeTrigger | treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true - | treeGridSorting:sortingExpressions:sortStrategy:pipeTrigger:true as pinnedData'> + | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true as pinnedData'>
Toggle Summary Position
+ - Sorting mode +
- Show summary on collapse
@@ -28,7 +31,7 @@ style="display: block; width: 500px">
Hide Grouped Columns +
+ (cellEditDone)="cellEditDone()" + [sortingOptions]="{mode: 'single'}">