From 7562cc5f56f7a600d80d1465025acf7deae73854 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 2 Jul 2025 15:02:21 +0300 Subject: [PATCH 001/106] chore(*): Cell merge POC. --- .../src/lib/grids/common/enums.ts | 14 ++ .../src/lib/grids/common/grid.interface.ts | 4 +- .../src/lib/grids/grid-base.directive.ts | 19 +- .../lib/grids/grid/grid-row.component.html | 48 ++++- .../src/lib/grids/grid/grid.component.html | 3 +- .../src/lib/grids/grid/grid.component.ts | 5 +- .../src/lib/grids/grid/grid.pipes.ts | 40 ++++ .../src/lib/grids/row.directive.ts | 10 + src/app/app.component.ts | 5 + src/app/app.routes.ts | 5 + .../grid-cellMerging.component.html | 15 ++ .../grid-cellMerging.component.scss | 19 ++ .../grid-cellMerging.component.ts | 190 ++++++++++++++++++ 13 files changed, 371 insertions(+), 6 deletions(-) create mode 100644 src/app/grid-cellMerging/grid-cellMerging.component.html create mode 100644 src/app/grid-cellMerging/grid-cellMerging.component.scss create mode 100644 src/app/grid-cellMerging/grid-cellMerging.component.ts diff --git a/projects/igniteui-angular/src/lib/grids/common/enums.ts b/projects/igniteui-angular/src/lib/grids/common/enums.ts index 88d277ccd39..bd66328c97e 100644 --- a/projects/igniteui-angular/src/lib/grids/common/enums.ts +++ b/projects/igniteui-angular/src/lib/grids/common/enums.ts @@ -73,6 +73,20 @@ export const GridSelectionMode = { } as const; export type GridSelectionMode = (typeof GridSelectionMode)[keyof typeof GridSelectionMode]; + +/** + * Enumeration representing different cell merging modes for the grid elements. + * - 'never': Never merge cells. + * - 'always': Always merge adjacent cells based on merge strategy. + * - 'onSort': Only merge cells in column that are sorted. + */ +export const GridCellMergeMode = { + never: 'never', + always: 'always', + onSort: 'onSort' +} as const; +export type GridCellMergeMode = (typeof GridCellMergeMode)[keyof typeof GridCellMergeMode]; + /** Enumeration representing different column display order options. */ export const ColumnDisplayOrder = { Alphabetical: 'Alphabetical', 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 3c8d8aa4f38..3b8d8d2e098 100644 --- a/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts +++ b/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts @@ -1,4 +1,4 @@ -import { ColumnPinningPosition, FilterMode, GridPagingMode, GridSelectionMode, GridSummaryCalculationMode, GridSummaryPosition, GridValidationTrigger, RowPinningPosition, Size } from './enums'; +import { ColumnPinningPosition, FilterMode, GridCellMergeMode, GridPagingMode, GridSelectionMode, GridSummaryCalculationMode, GridSummaryPosition, GridValidationTrigger, RowPinningPosition, Size } from './enums'; import { ISearchInfo, IGridCellEventArgs, IRowSelectionEventArgs, IColumnSelectionEventArgs, IPinColumnCancellableEventArgs, IColumnVisibilityChangedEventArgs, IColumnVisibilityChangingEventArgs, @@ -690,6 +690,7 @@ export interface GridServiceType { export interface GridType extends IGridDataBindable { /** Represents the locale of the grid: `USD`, `EUR`, `GBP`, `CNY`, `JPY`, etc. */ locale: string; + cellMergeMode: GridCellMergeMode; resourceStrings: IGridResourceStrings; /* blazorSuppress */ /** Represents the native HTML element itself */ @@ -1180,6 +1181,7 @@ export interface GridType extends IGridDataBindable { getEmptyRecordObjectFor(inRow: RowType): any; isSummaryRow(rec: any): boolean; isRecordPinned(rec: any): boolean; + isRecordMerged(rec: any): boolean; getInitialPinnedIndex(rec: any): number; isRecordPinnedByViewIndex(rowIndex: number): boolean; isColumnGrouped(fieldName: string): boolean; 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 5213151e54e..3398421a771 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -93,7 +93,8 @@ import { RowPinningPosition, GridPagingMode, GridValidationTrigger, - Size + Size, + GridCellMergeMode } from './common/enums'; import { IGridCellEventArgs, @@ -2911,6 +2912,14 @@ export abstract class IgxGridBaseDirective implements GridType, // } } + /** + * Gets/Sets cell merge mode. + * + */ + @WatchChanges() + @Input() + public cellMergeMode: GridCellMergeMode = GridCellMergeMode.never; + /** * Gets/Sets row selection mode * @@ -3635,6 +3644,14 @@ export abstract class IgxGridBaseDirective implements GridType, return this.getInitialPinnedIndex(rec) !== -1; } + /** + * @hidden + * @internal + */ + public isRecordMerged(rec) { + return rec.cellMergeMeta; + } + /** * @hidden * @internal diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index 7289bc16f29..99124fd2124 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -31,7 +31,19 @@ } } - + @if (this.hasMergedCells) { +
+ +
+ } + @else { + + } +
@if (pinnedColumns.length > 0 && !grid.isPinningToStart) { @for (col of pinnedColumns | igxNotGrouped; track trackPinnedColumn(col)) { @@ -158,6 +170,40 @@ + + + + + () }; + for (const col of visibleColumns) { + recData.cellMergeMeta.set(col.field, { rowSpan: 1 }); + //TODO condition can be a strategy or some callback that the user can set. + //TODO can also be limited to only sorted columns + if ( prev && prev.recordRef[col.field] === rec[col.field]) { + const root = prev.cellMergeMeta.get(col.field)?.root ?? prev; + root.cellMergeMeta.get(col.field).rowSpan += 1; + recData.cellMergeMeta.get(col.field).root = root; + } + } + prev = recData; + result.push(recData); + } + return result; + } +} + +export interface IMergeByResult { + rowSpan: number; + root?: any; +} + /** * @hidden */ diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index b2118b1f15e..a353b728c6e 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -27,6 +27,7 @@ import { mergeWith } from 'lodash-es'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { trackByIdentity } from '../core/utils'; +import { IMergeByResult } from './grid/grid.pipes'; @Directive({ selector: '[igxRowBaseComponent]', @@ -117,6 +118,10 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { return this.grid.isRecordPinned(this.data); } + public get hasMergedCells(): boolean { + return this.grid.isRecordMerged(this.data); + } + /** * Gets the expanded state of the row. * ```typescript @@ -592,6 +597,11 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { this.addAnimationEnd.emit(this); } + protected getMergeCellSpan(col: ColumnType){ + const rowCount = this.data.cellMergeMeta.get(col.field).rowSpan; + return `repeat(${rowCount},51px)`; + } + /** * @hidden */ diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c4637552c6b..c3d2d4d104b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -211,6 +211,11 @@ export class AppComponent implements OnInit { icon: 'view_column', name: 'Grid Cell Editing' }, + { + link: '/gridCellMerging', + icon: 'view_column', + name: 'Grid Cell Merging' + }, { link: '/gridClipboard', icon: 'insert_comment', diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 992516f1574..5234d396dc1 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -51,6 +51,7 @@ import { TimePickerSampleComponent } from './time-picker/time-picker.sample'; import { ToastShowcaseSampleComponent } from './toast-showcase/toast-showcase.sample'; import { VirtualForSampleComponent } from './virtual-for-directive/virtual-for.sample'; import { GridCellEditingComponent } from './grid-cellEditing/grid-cellEditing.component'; +import { GridCellMergingComponent } from './grid-cellMerging/grid-cellMerging.component'; import { GridSampleComponent } from './grid/grid.sample'; import { GridColumnMovingSampleComponent } from './grid-column-moving/grid-column-moving.sample'; import { GridColumnSelectionSampleComponent } from './grid-column-selection/grid-column-selection.sample'; @@ -419,6 +420,10 @@ export const appRoutes: Routes = [ path: 'gridCellEditing', component: GridCellEditingComponent }, + { + path: 'gridCellMerging', + component: GridCellMergingComponent + }, { path: 'gridConditionalCellStyling', component: GridCellStylingSampleComponent diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html new file mode 100644 index 00000000000..6b2628ac345 --- /dev/null +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -0,0 +1,15 @@ +

Grid with cell merge

+ + + + + + + + + + + + + + diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.scss b/src/app/grid-cellMerging/grid-cellMerging.component.scss new file mode 100644 index 00000000000..3d4037836d0 --- /dev/null +++ b/src/app/grid-cellMerging/grid-cellMerging.component.scss @@ -0,0 +1,19 @@ +.sample-actions { + display: flex; + flex-wrap: wrap; + margin: 1rem 0; + gap: 0.5rem; +} + +.density-chooser { + margin-bottom: 1rem; + + igx-buttongroup { + display: block; + width: 500px; + } +} + +.grid-size { + --ig-size: var(--ig-size-small); +} diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts new file mode 100644 index 00000000000..c2b9d9c0a88 --- /dev/null +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -0,0 +1,190 @@ +import { Component, HostBinding, ViewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + IgxColumnComponent, + IgxGridComponent, +} from 'igniteui-angular'; + +import { data, dataWithoutPK } from '../shared/data'; + +@Component({ + selector: 'app-grid-cellMerging', + templateUrl: 'grid-cellMerging.component.html', + styleUrl: 'grid-cellMerging.component.scss', + imports: [ + FormsModule, + IgxColumnComponent, + IgxGridComponent, + ] +}) +export class GridCellMergingComponent { + public data = [{ + ProductID: 1, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '10 boxes x 20 bags', + UnitPrice: '18.0000', + UnitsInStock: 39, + UnitsOnOrder: 0, + ReorderLevel: 10.567, + Discontinued: false, + OrderDate: null, + OrderDate2: new Date(1991, 2, 12, 18, 40, 50).toISOString() + }, { + ProductID: 2, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 3, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 4, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '20.0000', + UnitsInStock: 20, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 5, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 6, + ProductName: 'Chang', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 7, + ProductName: 'Chang', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 8, + ProductName: 'Chang', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 30, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 9, + ProductName: 'Aniseed Syrup', + SupplierID: 1, + CategoryID: 2, + QuantityPerUnit: '12 - 550 ml bottles', + UnitPrice: '10.0000', + UnitsInStock: 13, + UnitsOnOrder: 70, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2006-03-17').toISOString(), + OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() + }, + { + ProductID: 10, + ProductName: 'Chang', + SupplierID: 1, + CategoryID: 2, + QuantityPerUnit: '12 - 550 ml bottles', + UnitPrice: '10.0000', + UnitsInStock: 13, + UnitsOnOrder: 70, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2006-03-17').toISOString(), + OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() + }, + { + ProductID: 11, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 2, + QuantityPerUnit: '12 - 550 ml bottles', + UnitPrice: '10.0000', + UnitsInStock: 13, + UnitsOnOrder: 70, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2006-03-17').toISOString(), + OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() + }, + { + ProductID: 12, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 2, + QuantityPerUnit: '12 - 550 ml bottles', + UnitPrice: '10.0000', + UnitsInStock: 12, + UnitsOnOrder: 70, + ReorderLevel: 30, + Discontinued: false, + OrderDate: new Date('2006-03-17').toISOString(), + OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() + }]; + +} + From f42bb3c7d6f7f94938351138d24c046bf0ec4422 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 2 Jul 2025 16:47:10 +0300 Subject: [PATCH 002/106] chore(*): Minor tweaks to suggestion. --- .../lib/grids/grid/grid-row.component.html | 2 +- .../src/lib/grids/grid/grid.pipes.ts | 13 +- .../grid-cellMerging.component.ts | 167 ++++++++++++++++++ 3 files changed, 178 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index 99124fd2124..e3b00c36d28 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -194,7 +194,7 @@ [cellSelectionMode]="grid.cellSelection" [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)" [style.height.px]="data.cellMergeMeta.get(col.field).rowSpan * (this.grid.rowHeight + 1)" - [style.zIndex]="100" + [style.zIndex]="data.cellMergeMeta.get(col.field).rowSpan" [style.min-width]="col.resolvedWidth" [style.max-width]="col.resolvedWidth" [style.flex-basis]="col.resolvedWidth" diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index 2b6304904ff..2aa8eafd495 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -99,9 +99,15 @@ export class IgxGridCellMergePipe implements PipeTransform { //TODO condition can be a strategy or some callback that the user can set. //TODO can also be limited to only sorted columns if ( prev && prev.recordRef[col.field] === rec[col.field]) { - const root = prev.cellMergeMeta.get(col.field)?.root ?? prev; - root.cellMergeMeta.get(col.field).rowSpan += 1; - recData.cellMergeMeta.get(col.field).root = root; + // const root = prev.cellMergeMeta.get(col.field)?.root ?? prev; + // root.cellMergeMeta.get(col.field).rowSpan += 1; + // recData.cellMergeMeta.get(col.field).root = root; + recData.cellMergeMeta.get(col.field).prev = prev; + let curr = prev; + while(curr) { + curr.cellMergeMeta.get(col.field).rowSpan += 1; + curr = curr.cellMergeMeta.get(col.field).prev; + } } } prev = recData; @@ -114,6 +120,7 @@ export class IgxGridCellMergePipe implements PipeTransform { export interface IMergeByResult { rowSpan: number; root?: any; + prev?: any; } /** diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index c2b9d9c0a88..c4184a997ae 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -171,6 +171,173 @@ export class GridCellMergingComponent { OrderDate: new Date('2006-03-17').toISOString(), OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() }, + { + ProductID: 12, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 2, + QuantityPerUnit: '12 - 550 ml bottles', + UnitPrice: '10.0000', + UnitsInStock: 12, + UnitsOnOrder: 70, + ReorderLevel: 30, + Discontinued: false, + OrderDate: new Date('2006-03-17').toISOString(), + OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() + }, + { + ProductID: 1, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '10 boxes x 20 bags', + UnitPrice: '18.0000', + UnitsInStock: 39, + UnitsOnOrder: 0, + ReorderLevel: 10.567, + Discontinued: false, + OrderDate: null, + OrderDate2: new Date(1991, 2, 12, 18, 40, 50).toISOString() + }, { + ProductID: 2, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 3, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 4, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '20.0000', + UnitsInStock: 20, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 5, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 6, + ProductName: 'Chang', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 7, + ProductName: 'Chang', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 8, + ProductName: 'Chang', + SupplierID: 1, + CategoryID: 1, + QuantityPerUnit: '24 - 12 oz bottles', + UnitPrice: '19.0000', + UnitsInStock: 17, + UnitsOnOrder: 40, + ReorderLevel: 30, + Discontinued: false, + OrderDate: new Date('2003-03-17').toISOString(), + OrderDate2: new Date('2003-03-17').toISOString() + }, + { + ProductID: 9, + ProductName: 'Aniseed Syrup', + SupplierID: 1, + CategoryID: 2, + QuantityPerUnit: '12 - 550 ml bottles', + UnitPrice: '10.0000', + UnitsInStock: 13, + UnitsOnOrder: 70, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2006-03-17').toISOString(), + OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() + }, + { + ProductID: 10, + ProductName: 'Chang', + SupplierID: 1, + CategoryID: 2, + QuantityPerUnit: '12 - 550 ml bottles', + UnitPrice: '10.0000', + UnitsInStock: 13, + UnitsOnOrder: 70, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2006-03-17').toISOString(), + OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() + }, + { + ProductID: 11, + ProductName: 'Chai', + SupplierID: 1, + CategoryID: 2, + QuantityPerUnit: '12 - 550 ml bottles', + UnitPrice: '10.0000', + UnitsInStock: 13, + UnitsOnOrder: 70, + ReorderLevel: 25, + Discontinued: false, + OrderDate: new Date('2006-03-17').toISOString(), + OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() + }, { ProductID: 12, ProductName: 'Chai', From 870e7f61f966e3353502d9f93342d0c1e72caa08 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 3 Jul 2025 17:46:06 +0300 Subject: [PATCH 003/106] chore(*): Implement with rows that are retained outside the virt.frame. --- .../src/lib/grids/grid-base.directive.ts | 5 +++++ .../src/lib/grids/grid/grid-row.component.html | 1 - .../src/lib/grids/grid/grid.component.html | 8 ++++++++ .../src/lib/grids/grid/grid.component.ts | 11 +++++++++++ .../src/lib/grids/grid/grid.pipes.ts | 18 +++++++++--------- .../grid-cellMerging.component.html | 4 +--- 6 files changed, 34 insertions(+), 13 deletions(-) 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 3398421a771..659e5e6e18c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3652,6 +3652,11 @@ export abstract class IgxGridBaseDirective implements GridType, return rec.cellMergeMeta; } + public getMergeCellOffset(rec) { + const index = this.verticalScrollContainer.igxForOf.indexOf(rec); + return -(this.verticalScrollContainer.scrollPosition - this.verticalScrollContainer.getScrollForIndex(index)); + } + /** * @hidden * @internal diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index e3b00c36d28..d477ce770ad 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -199,7 +199,6 @@ [style.max-width]="col.resolvedWidth" [style.flex-basis]="col.resolvedWidth" [width]="col.getCellWidth()" - [style.background]="'white'" #cell>
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 cf02d0ab7d0..fc0df2e4644 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -47,6 +47,14 @@ } + + + @for (rowData of mergedData; track rowData; let rowIndex = $index) { + + + } @if (data | gridTransaction:id:pipeTrigger diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts index 927692b4a45..d1d0a3fa116 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts @@ -389,6 +389,17 @@ export class IgxGridComponent extends IgxGridBaseDirective implements GridType, private _groupByRowSelectorTemplate: TemplateRef; private _detailTemplate; + public get mergedData() { + const startIndex = this.verticalScrollContainer.state.startIndex; + const chunkSize = this.verticalScrollContainer.state.chunkSize; + const mergeRecs = this.verticalScrollContainer.igxForOf?.filter((x, index) => { + if (!x.cellMergeMeta) { return false;} + const maxSpan = Math.max(...x.cellMergeMeta.values().toArray().map(x => x.rowSpan)); + return startIndex > index && startIndex <= (index + maxSpan) && startIndex + chunkSize >= (index + maxSpan); + }); + return mergeRecs; + } + /** * Gets/Sets the array of data that populates the component. diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index 2aa8eafd495..d9e5900c410 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -99,15 +99,15 @@ export class IgxGridCellMergePipe implements PipeTransform { //TODO condition can be a strategy or some callback that the user can set. //TODO can also be limited to only sorted columns if ( prev && prev.recordRef[col.field] === rec[col.field]) { - // const root = prev.cellMergeMeta.get(col.field)?.root ?? prev; - // root.cellMergeMeta.get(col.field).rowSpan += 1; - // recData.cellMergeMeta.get(col.field).root = root; - recData.cellMergeMeta.get(col.field).prev = prev; - let curr = prev; - while(curr) { - curr.cellMergeMeta.get(col.field).rowSpan += 1; - curr = curr.cellMergeMeta.get(col.field).prev; - } + const root = prev.cellMergeMeta.get(col.field)?.root ?? prev; + root.cellMergeMeta.get(col.field).rowSpan += 1; + recData.cellMergeMeta.get(col.field).root = root; + // recData.cellMergeMeta.get(col.field).prev = prev; + // let curr = prev; + // while(curr) { + // curr.cellMergeMeta.get(col.field).rowSpan += 1; + // curr = curr.cellMergeMeta.get(col.field).prev; + // } } } prev = recData; diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 6b2628ac345..bc1c1f1bc9c 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -1,5 +1,5 @@

Grid with cell merge

- + @@ -10,6 +10,4 @@

Grid with cell merge

- -
From bb4e39fcce4350ab6111f6291c00f5b72537c845 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 7 Jul 2025 14:46:10 +0300 Subject: [PATCH 004/106] chore(*): Fix border styles. --- projects/igniteui-angular/src/lib/grids/common/pipes.ts | 2 +- .../src/lib/grids/grid/grid-row.component.html | 8 +++++--- projects/igniteui-angular/src/lib/grids/row.directive.ts | 4 ++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/common/pipes.ts b/projects/igniteui-angular/src/lib/grids/common/pipes.ts index 818a0ea3854..371245c96ad 100644 --- a/projects/igniteui-angular/src/lib/grids/common/pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/common/pipes.ts @@ -126,7 +126,7 @@ export class IgxGridRowClassesPipe implements PipeTransform { [dirty, 'igx-grid__tr--edited'], [deleted, 'igx-grid__tr--deleted'], [dragging, 'igx-grid__tr--drag'], - [mrl, 'igx-grid__tr--mrl'], + [mrl || _rowData.cellMergeMeta, 'igx-grid__tr--mrl'], // Tree grid only [filteredOut, 'igx-grid__tr--filtered'] ]; diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index d477ce770ad..6111ac5ec15 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -32,9 +32,10 @@ } @if (this.hasMergedCells) { -
@@ -173,6 +174,7 @@ Date: Mon, 7 Jul 2025 15:25:33 +0300 Subject: [PATCH 005/106] chore(*): Add handling for variable row height. --- .../src/lib/grids/grid/grid-row.component.html | 4 ++-- projects/igniteui-angular/src/lib/grids/row.directive.ts | 6 ++++-- src/app/grid-cellMerging/grid-cellMerging.component.html | 7 +++++++ src/app/grid-cellMerging/grid-cellMerging.component.ts | 4 ++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index 6111ac5ec15..a2391599eed 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -32,11 +32,11 @@ } @if (this.hasMergedCells) { -
diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index 7a44f1851d1..9ebf02488d6 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -599,11 +599,13 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { protected getMergeCellSpan(col: ColumnType){ const rowCount = this.data.cellMergeMeta.get(col.field).rowSpan; - return `repeat(${rowCount},51px)`; + const rowH = this.grid.verticalScrollContainer.getSizeAt(this.index); + return `repeat(${rowCount},${rowH}px)`; } protected getRowHeight() { - return this.grid.rowHeight; + const size = this.grid.verticalScrollContainer.getSizeAt(this.index) - 1; + return size || this.grid.rowHeight; } /** diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index bc1c1f1bc9c..b5f66aaf5dc 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -1,6 +1,13 @@

Grid with cell merge

+ +
+ +
+ +
+
diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index c4184a997ae..0d79c827d11 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -1,6 +1,8 @@ import { Component, HostBinding, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { + IgxButtonDirective, + IgxCellTemplateDirective, IgxColumnComponent, IgxGridComponent, } from 'igniteui-angular'; @@ -15,6 +17,8 @@ import { data, dataWithoutPK } from '../shared/data'; FormsModule, IgxColumnComponent, IgxGridComponent, + IgxCellTemplateDirective, + IgxButtonDirective ] }) export class GridCellMergingComponent { From 4667bb083ba7d63982b95b333172fff51f9b5221 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 7 Jul 2025 15:44:31 +0300 Subject: [PATCH 006/106] chore(*): Add handling if different rows have different sizes. --- projects/igniteui-angular/src/lib/grids/row.directive.ts | 8 ++++++-- src/app/grid-cellMerging/grid-cellMerging.component.html | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index 9ebf02488d6..d61923064bf 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -599,8 +599,12 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { protected getMergeCellSpan(col: ColumnType){ const rowCount = this.data.cellMergeMeta.get(col.field).rowSpan; - const rowH = this.grid.verticalScrollContainer.getSizeAt(this.index); - return `repeat(${rowCount},${rowH}px)`; + let sizeSpans = ""; + for (let index = this.index; index < this.index + rowCount; index++) { + const size = this.grid.verticalScrollContainer.getSizeAt(index); + sizeSpans += size + 'px '; + } + return `${sizeSpans}`; } protected getRowHeight() { diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index b5f66aaf5dc..a745f0cb5ce 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -4,8 +4,11 @@

Grid with cell merge

-
- + @if (cell.row.index % 2 == 0) { +
+ + } +
From febbd43e19776bcfba9cd44e9e83ec6a42a93595 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 7 Jul 2025 16:53:10 +0300 Subject: [PATCH 007/106] chore(*): Extract hardcoded styles in class. --- .../styles/components/grid/_grid-component.scss | 4 ++++ .../core/styles/components/grid/_grid-theme.scss | 15 ++++++++++----- .../src/lib/grids/grid/grid-row.component.html | 2 -- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss index f446ffbe128..93737fc219c 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss @@ -296,6 +296,10 @@ @extend %igx-grid__td--edited !optional; } + @include e(td, $m: merged) { + @extend %igx-grid__td--merged !optional; + } + @include e(td, $m: editing) { @extend %igx-grid__td--editing !optional; } diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index 0166b635863..47123b240b3 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1872,6 +1872,11 @@ } } + %igx-grid__td--merged { + z-index: 1; + grid-row: 1 / -1; + } + %igx-grid__tr--deleted { %grid-cell-text { font-style: italic; @@ -2084,7 +2089,7 @@ .sort-icon { color: var-get($theme, 'header-selected-text-color'); - + ::after { background: var-get($theme, 'header-selected-background'); } @@ -2112,7 +2117,7 @@ &%igx-grid-th--sorted { .sort-icon { color: var-get($theme, 'header-selected-text-color'); - + > igx-icon { color: inherit; } @@ -2120,7 +2125,7 @@ &:focus, &:hover { color: var-get($theme, 'header-selected-text-color'); - + > igx-icon { color: inherit; } @@ -2177,14 +2182,14 @@ .sort-icon { opacity: 1; color: var-get($theme, 'sorted-header-icon-color'); - + > igx-icon { color: inherit; } &:hover { color: var-get($theme, 'sortable-header-icon-hover-color'); - + > igx-icon { color: inherit; } diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index a2391599eed..c87ab17861c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -195,8 +195,6 @@ [active]="isCellActive(col.visibleIndex)" [cellSelectionMode]="grid.cellSelection" [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)" - [style.gridRow]="'1 / -1'" - [style.zIndex]="data.cellMergeMeta.get(col.field).rowSpan" [style.min-width]="col.resolvedWidth" [style.max-width]="col.resolvedWidth" [style.flex-basis]="col.resolvedWidth" From 4c5d922f1e287f9ac160df2c1385b4f42eb7c70a Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 7 Jul 2025 17:27:53 +0300 Subject: [PATCH 008/106] chore(*): Adjust API and members. --- .../src/lib/grids/columns/column.component.ts | 9 +++++++++ .../src/lib/grids/common/enums.ts | 1 - .../src/lib/grids/common/grid.interface.ts | 1 + .../src/lib/grids/grid-base.directive.ts | 2 +- .../src/lib/grids/grid/grid-row.component.html | 8 ++++---- .../src/lib/grids/grid/grid.pipes.ts | 18 ++++++++---------- .../grid-cellMerging.component.html | 6 +++--- 7 files changed, 26 insertions(+), 19 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts index 3482a5076f1..0809995f477 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts @@ -107,6 +107,15 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy return this._field; } + /** + * Sets/gets whether to merge cells in this column. + * ```html + * + * ``` + * + */ + @Input() + public merge = false; /** * @hidden @internal diff --git a/projects/igniteui-angular/src/lib/grids/common/enums.ts b/projects/igniteui-angular/src/lib/grids/common/enums.ts index bd66328c97e..fd19aa101ca 100644 --- a/projects/igniteui-angular/src/lib/grids/common/enums.ts +++ b/projects/igniteui-angular/src/lib/grids/common/enums.ts @@ -81,7 +81,6 @@ export type GridSelectionMode = (typeof GridSelectionMode)[keyof typeof GridSele * - 'onSort': Only merge cells in column that are sorted. */ export const GridCellMergeMode = { - never: 'never', always: 'always', onSort: 'onSort' } as const; 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 3b8d8d2e098..259e621c056 100644 --- a/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts +++ b/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts @@ -453,6 +453,7 @@ export interface ColumnType extends FieldType { pinned: boolean; /** Indicates if the column is currently expanded or collapsed. If the value is true, the column is expanded */ expanded: boolean; + merge: boolean; /** Indicates if the column is currently selected. If the value is true, the column is selected */ selected: boolean; /** Indicates if the column can be selected. If the value is true, the column can be selected */ 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 659e5e6e18c..b5a0123f568 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -2918,7 +2918,7 @@ export abstract class IgxGridBaseDirective implements GridType, */ @WatchChanges() @Input() - public cellMergeMode: GridCellMergeMode = GridCellMergeMode.never; + public cellMergeMode: GridCellMergeMode = GridCellMergeMode.onSort; /** * Gets/Sets row selection mode diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index c87ab17861c..0489078ac7d 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -32,11 +32,11 @@ } @if (this.hasMergedCells) { -
@@ -174,7 +174,7 @@ x.merge && (mergeMode ==='always' || + (mergeMode === 'onSort' && !!sortExpr.find( x=> x.fieldName === x.fieldName))) + ); + if (columnToMerge.length === 0) { return collection; } - const visibleColumns = this.grid.visibleColumns; let prev = null; let result = []; for (const rec of collection) { let recData = { recordRef: rec, cellMergeMeta: new Map() }; - for (const col of visibleColumns) { + for (const col of columnToMerge) { recData.cellMergeMeta.set(col.field, { rowSpan: 1 }); //TODO condition can be a strategy or some callback that the user can set. - //TODO can also be limited to only sorted columns if ( prev && prev.recordRef[col.field] === rec[col.field]) { const root = prev.cellMergeMeta.get(col.field)?.root ?? prev; root.cellMergeMeta.get(col.field).rowSpan += 1; recData.cellMergeMeta.get(col.field).root = root; - // recData.cellMergeMeta.get(col.field).prev = prev; - // let curr = prev; - // while(curr) { - // curr.cellMergeMeta.get(col.field).rowSpan += 1; - // curr = curr.cellMergeMeta.get(col.field).prev; - // } } } prev = recData; diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index a745f0cb5ce..ca4bbb3d153 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -12,11 +12,11 @@

Grid with cell merge

- + - + - + From a42b192b63d62753ffc0f19b30a103ccd9ee7e8f Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 8 Jul 2025 16:33:27 +0300 Subject: [PATCH 009/106] chore(*): Add mergeStrategy for grid and merging comparer for column. --- .../src/lib/data-operations/data-util.ts | 19 +++++- .../src/lib/data-operations/merge-strategy.ts | 67 +++++++++++++++++++ .../src/lib/grids/columns/column.component.ts | 26 +++++++ .../src/lib/grids/common/grid.interface.ts | 3 + .../src/lib/grids/grid-base.directive.ts | 13 ++++ .../src/lib/grids/grid/grid.pipes.ts | 23 +------ .../src/lib/grids/row.directive.ts | 1 - .../grid-cellMerging.component.html | 6 +- .../grid-cellMerging.component.ts | 24 +++---- 9 files changed, 143 insertions(+), 39 deletions(-) create mode 100644 projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts 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 cc0e0d5ad1c..d3e66f19248 100644 --- a/projects/igniteui-angular/src/lib/data-operations/data-util.ts +++ b/projects/igniteui-angular/src/lib/data-operations/data-util.ts @@ -8,7 +8,7 @@ import { IGroupingState } from './groupby-state.interface'; import { mergeObjects } from '../core/utils'; import { Transaction, TransactionType, HierarchicalTransaction } from '../services/transaction/transaction'; import { getHierarchy, isHierarchyMatch } from './operations'; -import { GridType } from '../grids/common/grid.interface'; +import { ColumnType, GridType } from '../grids/common/grid.interface'; import { ITreeGridRecord } from '../grids/tree-grid/tree-grid.interfaces'; import { ISortingExpression } from './sorting-strategy'; import { @@ -20,6 +20,7 @@ import { } from '../grids/common/strategy'; import { DefaultDataCloneStrategy, IDataCloneStrategy } from '../data-operations/data-clone-strategy'; import { IGroupingExpression } from './grouping-expression.interface'; +import { DefaultMergeStrategy, IGridMergeStrategy } from './merge-strategy'; /** * @hidden @@ -37,6 +38,13 @@ import { IGroupingExpression } from './grouping-expression.interface'; } as const; export type DataType = (typeof DataType)[keyof typeof DataType]; + +export interface IMergeByResult { + rowSpan: number; + root?: any; + prev?: any; +} + /** * @hidden */ @@ -90,6 +98,15 @@ export class DataUtil { return grouping.groupBy(data, state, grid, groupsRecords, fullResult); } + public static merge(data: T[], columns: ColumnType[], strategy: IGridMergeStrategy = new DefaultMergeStrategy(), grid: GridType = null, + ): any[] { + let result = []; + for (const col of columns) { + strategy.merge(data, col.field, col.mergingComparer, result); + } + return result; +} + public static page(data: T[], state: IPagingState, dataLength?: number): T[] { if (!state) { return data; diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts new file mode 100644 index 00000000000..c9afd6fc3f9 --- /dev/null +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -0,0 +1,67 @@ +import { GridCellMergeMode, IMergeByResult } from 'igniteui-angular'; +import type { KeyOfOrString } from '../core/types'; +import { IBaseEventArgs } from '../core/utils'; +import { ColumnType, GridType } from '../grids/common/grid.interface'; + + +export interface IGridMergeStrategy { + /* blazorSuppress */ + merge: ( + data: any[], + field: string, + comparer: (prevRecord: any, currentRecord: any, field: string) => boolean, + result: any[] + ) => any[]; +} + +export class DefaultMergeStrategy implements IGridMergeStrategy { + protected static _instance: DefaultMergeStrategy = null; + + public static instance(): DefaultMergeStrategy { + return this._instance || (this._instance = new this()); + } + + /* blazorSuppress */ + public merge( + data: any[], + field: string, + comparer: (prevRecord: any, record: any, field: string) => boolean = this.comparer, + result: any[] + ) { + let prev = null; + let index = 0; + for (const rec of data) { + const recData = result[index]; + let recToUpdateData = recData ?? { recordRef: rec, cellMergeMeta: new Map() }; + recToUpdateData.cellMergeMeta.set(field, { rowSpan: 1 }); + if (prev && comparer(prev.recordRef, recToUpdateData.recordRef, field)) { + const root = prev.cellMergeMeta.get(field)?.root ?? prev; + root.cellMergeMeta.get(field).rowSpan += 1; + recToUpdateData.cellMergeMeta.get(field).root = root; + } + prev = recToUpdateData; + if (!recData) { + result.push(recToUpdateData); + } + index++; + } + return result; + } + + /* blazorSuppress */ + public comparer(prevRecord: any, record: any, field: string): boolean { + const a = prevRecord[field]; + const b = record[field]; + const an = (a === null || a === undefined); + const bn = (b === null || b === undefined); + if (an) { + if (bn) { + return true; + } + return false; + } else if (bn) { + return false; + } + return a === b; + } +} diff --git a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts index 0809995f477..58b3de78586 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts @@ -1209,6 +1209,30 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy this._sortStrategy = classRef; } + /* blazorSuppress */ + /** + * Gets the function that compares values for merging. + * ```typescript + * let mergingComparer = this.column.mergingComparer' + * ``` + */ + @Input() + public get mergingComparer(): (prevRecord: any, record: any, field: string) => boolean { + return this._mergingComparer; + } + + /* blazorSuppress */ + /** + * Sets a custom function to compare values for merging. + * ```typescript + * this.column.mergingComparer = (prevRecord: any, record: any, field: string) => { return prevRecord[field] === record[field]; } + * ``` + */ + public set mergingComparer(funcRef: (prevRecord: any, record: any, field: string) => boolean) { + this._mergingComparer = funcRef; + } + + /* blazorSuppress */ /** * Gets the function that compares values for grouping. @@ -1849,6 +1873,8 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy * @hidden */ protected _groupingComparer: (a: any, b: any, currRec?: any, groupRec?: any) => number; + + protected _mergingComparer: (prevRecord: any, record: any, field: string) => boolean; /** * @hidden */ 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 259e621c056..308c6be4084 100644 --- a/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts +++ b/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts @@ -38,6 +38,7 @@ import { IDimensionsChange, IPivotConfiguration, IPivotDimension, IPivotKeys, IP import { IDataCloneStrategy } from '../../data-operations/data-clone-strategy'; import { FormControl, FormGroup, ValidationErrors } from '@angular/forms'; import { IgxGridValidationService } from '../grid/grid-validation.service'; +import { IGridMergeStrategy } from '../../data-operations/merge-strategy'; export const IGX_GRID_BASE = /*@__PURE__*/new InjectionToken('IgxGridBaseToken'); export const IGX_GRID_SERVICE_BASE = /*@__PURE__*/new InjectionToken('IgxGridServiceBaseToken'); @@ -335,6 +336,7 @@ export interface ColumnType extends FieldType { /** @hidden @internal */ headerCell: any; validators: any[]; + mergingComparer: (prevRecord: any, record: any, field: string) => boolean; /** * The template reference for the custom header of the column @@ -692,6 +694,7 @@ export interface GridType extends IGridDataBindable { /** Represents the locale of the grid: `USD`, `EUR`, `GBP`, `CNY`, `JPY`, etc. */ locale: string; cellMergeMode: GridCellMergeMode; + mergeStrategy: IGridMergeStrategy; resourceStrings: IGridResourceStrings; /* blazorSuppress */ /** Represents the native HTML element itself */ 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 b5a0123f568..b2baeb1cc30 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -185,6 +185,7 @@ import { IgxGridValidationService } from './grid/grid-validation.service'; import { getCurrentResourceStrings } from '../core/i18n/resources'; import { isTree, recreateTree, recreateTreeFromFields } from '../data-operations/expressions-tree-util'; import { getUUID } from './common/random'; +import { IGridMergeStrategy } from '../data-operations/merge-strategy'; interface IMatchInfoCache { row: any; @@ -2494,6 +2495,18 @@ export abstract class IgxGridBaseDirective implements GridType, this._sortingStrategy = value; } + + /** + * Gets/Sets the merge strategy of the grid. + * + * @example + * ```html + * + * ``` + */ + @Input() + public mergeStrategy: IGridMergeStrategy; + /** * Gets/Sets the sorting options - single or multiple sorting. * Accepts an `ISortingOptions` object with any of the `mode` properties. diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index 73d87e9c00d..43267186900 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -95,32 +95,11 @@ export class IgxGridCellMergePipe implements PipeTransform { if (columnToMerge.length === 0) { return collection; } - let prev = null; - let result = []; - for (const rec of collection) { - let recData = { recordRef: rec, cellMergeMeta: new Map() }; - for (const col of columnToMerge) { - recData.cellMergeMeta.set(col.field, { rowSpan: 1 }); - //TODO condition can be a strategy or some callback that the user can set. - if ( prev && prev.recordRef[col.field] === rec[col.field]) { - const root = prev.cellMergeMeta.get(col.field)?.root ?? prev; - root.cellMergeMeta.get(col.field).rowSpan += 1; - recData.cellMergeMeta.get(col.field).root = root; - } - } - prev = recData; - result.push(recData); - } + const result = DataUtil.merge(cloneArray(collection), columnToMerge, this.grid.mergeStrategy, this.grid); return result; } } -export interface IMergeByResult { - rowSpan: number; - root?: any; - prev?: any; -} - /** * @hidden */ diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index d61923064bf..38dde79226c 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -27,7 +27,6 @@ import { mergeWith } from 'lodash-es'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { trackByIdentity } from '../core/utils'; -import { IMergeByResult } from './grid/grid.pipes'; @Directive({ selector: '[igxRowBaseComponent]', diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index ca4bbb3d153..9d7d1814595 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -3,11 +3,11 @@

Grid with cell merge

- - @if (cell.row.index % 2 == 0) { + +
diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index 0d79c827d11..7443240d731 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -190,7 +190,7 @@ export class GridCellMergingComponent { OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() }, { - ProductID: 1, + ProductID: 13, ProductName: 'Chai', SupplierID: 1, CategoryID: 1, @@ -203,7 +203,7 @@ export class GridCellMergingComponent { OrderDate: null, OrderDate2: new Date(1991, 2, 12, 18, 40, 50).toISOString() }, { - ProductID: 2, + ProductID: 14, ProductName: 'Chai', SupplierID: 1, CategoryID: 1, @@ -217,7 +217,7 @@ export class GridCellMergingComponent { OrderDate2: new Date('2003-03-17').toISOString() }, { - ProductID: 3, + ProductID: 15, ProductName: 'Chai', SupplierID: 1, CategoryID: 1, @@ -231,7 +231,7 @@ export class GridCellMergingComponent { OrderDate2: new Date('2003-03-17').toISOString() }, { - ProductID: 4, + ProductID: 16, ProductName: 'Chai', SupplierID: 1, CategoryID: 1, @@ -245,7 +245,7 @@ export class GridCellMergingComponent { OrderDate2: new Date('2003-03-17').toISOString() }, { - ProductID: 5, + ProductID: 17, ProductName: 'Chai', SupplierID: 1, CategoryID: 1, @@ -259,7 +259,7 @@ export class GridCellMergingComponent { OrderDate2: new Date('2003-03-17').toISOString() }, { - ProductID: 6, + ProductID: 18, ProductName: 'Chang', SupplierID: 1, CategoryID: 1, @@ -273,7 +273,7 @@ export class GridCellMergingComponent { OrderDate2: new Date('2003-03-17').toISOString() }, { - ProductID: 7, + ProductID: 19, ProductName: 'Chang', SupplierID: 1, CategoryID: 1, @@ -287,7 +287,7 @@ export class GridCellMergingComponent { OrderDate2: new Date('2003-03-17').toISOString() }, { - ProductID: 8, + ProductID: 20, ProductName: 'Chang', SupplierID: 1, CategoryID: 1, @@ -301,7 +301,7 @@ export class GridCellMergingComponent { OrderDate2: new Date('2003-03-17').toISOString() }, { - ProductID: 9, + ProductID: 21, ProductName: 'Aniseed Syrup', SupplierID: 1, CategoryID: 2, @@ -315,7 +315,7 @@ export class GridCellMergingComponent { OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() }, { - ProductID: 10, + ProductID: 22, ProductName: 'Chang', SupplierID: 1, CategoryID: 2, @@ -329,7 +329,7 @@ export class GridCellMergingComponent { OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() }, { - ProductID: 11, + ProductID: 23, ProductName: 'Chai', SupplierID: 1, CategoryID: 2, @@ -343,7 +343,7 @@ export class GridCellMergingComponent { OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() }, { - ProductID: 12, + ProductID: 24, ProductName: 'Chai', SupplierID: 1, CategoryID: 2, From 13619527242455f4da928093c97ec0c63b7bec48 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 9 Jul 2025 13:12:20 +0300 Subject: [PATCH 010/106] chore(*): Adjust pipe triggers. --- .../src/lib/grids/grid/grid.component.html | 2 +- .../igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) 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 fc0df2e4644..3af125f1e03 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -89,7 +89,7 @@ | gridDetails:hasDetails:expansionStates:pipeTrigger | gridAddRow:false:pipeTrigger | gridRowPinning:id:false:pipeTrigger - | gridCellMerge:pipeTrigger" + | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:pipeTrigger" let-rowIndex="index" [igxForScrollOrientation]="'vertical'" [igxForScrollContainer]="verticalScroll" [igxForContainerSize]="calcHeight" [igxForItemSize]="hasColumnLayouts ? rowHeight * multiRowLayoutRowSize + 1 : renderedRowHeight" diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index 43267186900..77d52bcbad1 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -5,7 +5,7 @@ import { IGroupByExpandState } from '../../data-operations/groupby-expand-state. import { IGroupByResult } from '../../data-operations/grouping-result.interface'; import { IFilteringExpressionsTree, FilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree'; import { IGroupingExpression } from '../../data-operations/grouping-expression.interface'; -import { GridType, IGX_GRID_BASE } from '../common/grid.interface'; +import { ColumnType, GridType, IGX_GRID_BASE } from '../common/grid.interface'; import { FilterUtil, IFilteringStrategy } from '../../data-operations/filtering-strategy'; import { ISortingExpression } from '../../data-operations/sorting-strategy'; import { IGridSortingStrategy, IGridGroupingStrategy } from '../common/strategy'; @@ -85,10 +85,8 @@ export class IgxGridCellMergePipe implements PipeTransform { constructor(@Inject(IGX_GRID_BASE) private grid: GridType) { } - public transform(collection: any, _pipeTrigger: number) { - const mergeMode = this.grid.cellMergeMode; - const sortExpr = this.grid.sortingExpressions; - const columnToMerge = this.grid.visibleColumns.filter( + public transform(collection: any, visibleColumns: ColumnType[], mergeMode: GridCellMergeMode, sortExpr: ISortingExpression[], _pipeTrigger: number) { + const columnToMerge = visibleColumns.filter( x => x.merge && (mergeMode ==='always' || (mergeMode === 'onSort' && !!sortExpr.find( x=> x.fieldName === x.fieldName))) ); From 407357f5e58adcd6eb26a18d9d11cdae605cad88 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 9 Jul 2025 14:42:42 +0300 Subject: [PATCH 011/106] chore(*): Re-use cell templates. --- .../src/lib/grids/common/pipes.ts | 3 +- .../src/lib/grids/grid-base.directive.ts | 2 +- .../lib/grids/grid/grid-row.component.html | 42 +++---------------- .../src/lib/grids/grid/grid.component.html | 14 +++---- .../src/lib/grids/grid/grid.component.ts | 5 ++- .../hierarchical-grid.component.html | 4 +- .../pivot-grid/pivot-grid.component.html | 2 +- .../src/lib/grids/row.directive.ts | 10 ++++- .../grids/tree-grid/tree-grid.component.html | 4 +- .../grid-cellMerging.component.html | 6 ++- .../grid-cellMerging.component.ts | 4 +- 11 files changed, 39 insertions(+), 57 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/common/pipes.ts b/projects/igniteui-angular/src/lib/grids/common/pipes.ts index 371245c96ad..27d0667fbeb 100644 --- a/projects/igniteui-angular/src/lib/grids/common/pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/common/pipes.ts @@ -115,6 +115,7 @@ export class IgxGridRowClassesPipe implements PipeTransform { dragging: boolean, index: number, mrl: boolean, + merged: boolean, filteredOut: boolean, _rowData: any, _: number @@ -126,7 +127,7 @@ export class IgxGridRowClassesPipe implements PipeTransform { [dirty, 'igx-grid__tr--edited'], [deleted, 'igx-grid__tr--deleted'], [dragging, 'igx-grid__tr--drag'], - [mrl || _rowData.cellMergeMeta, 'igx-grid__tr--mrl'], + [mrl || merged, 'igx-grid__tr--mrl'], // Tree grid only [filteredOut, 'igx-grid__tr--filtered'] ]; 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 b2baeb1cc30..44db042c792 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3662,7 +3662,7 @@ export abstract class IgxGridBaseDirective implements GridType, * @internal */ public isRecordMerged(rec) { - return rec.cellMergeMeta; + return rec?.cellMergeMeta; } public getMergeCellOffset(rec) { diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index 0489078ac7d..dd1eac61f3f 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -32,13 +32,13 @@ } @if (this.hasMergedCells) { -
- +
} @else { @@ -108,6 +108,7 @@ - - - - - } @@ -105,15 +105,15 @@ - - + - + diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts index d1d0a3fa116..8395d09cf5e 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts @@ -960,13 +960,14 @@ export class IgxGridComponent extends IgxGridBaseDirective implements GridType, } } return { - $implicit: this.isGhostRecord(rowData) ? rowData.recordRef : rowData, + $implicit: this.isGhostRecord(rowData) || this.isRecordMerged(rowData) ? rowData.recordRef : rowData, index: this.getDataViewIndex(rowIndex, pinned), templateID: { type: this.isGroupByRecord(rowData) ? 'groupRow' : this.isSummaryRow(rowData) ? 'summaryRow' : 'dataRow', id: null }, - disabled: this.isGhostRecord(rowData) + disabled: this.isGhostRecord(rowData), + metaData: rowData }; } 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 d5f31ada6b4..59be7cefe5b 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 @@ -75,14 +75,14 @@ diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.html b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.html index 9ca659192f9..95089b96554 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.html @@ -52,7 +52,7 @@
diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index 38dde79226c..d4e4b6ae5c6 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -45,6 +45,12 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { @HostBinding('attr.role') public role = 'row'; + /** + * @hidden + */ + @Input() + public metaData: any; + /** * The data passed to the row component. * @@ -118,7 +124,7 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { } public get hasMergedCells(): boolean { - return this.grid.isRecordMerged(this.data); + return this.grid.isRecordMerged(this.metaData); } /** @@ -597,7 +603,7 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { } protected getMergeCellSpan(col: ColumnType){ - const rowCount = this.data.cellMergeMeta.get(col.field).rowSpan; + const rowCount = this.metaData.cellMergeMeta.get(col.field).rowSpan; let sizeSpans = ""; for (let index = this.index; index < this.index + rowCount; index++) { const size = this.grid.verticalScrollContainer.getSizeAt(index); 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 2521cc28e58..5db61b5629c 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 @@ -78,13 +78,13 @@ diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 9d7d1814595..da15f25881d 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -1,5 +1,5 @@

Grid with cell merge

- +
@@ -12,7 +12,7 @@

Grid with cell merge

- + @@ -20,4 +20,6 @@

Grid with cell merge

+ +
diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index 7443240d731..527180f1a09 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -5,6 +5,7 @@ import { IgxCellTemplateDirective, IgxColumnComponent, IgxGridComponent, + IgxPaginatorComponent, } from 'igniteui-angular'; import { data, dataWithoutPK } from '../shared/data'; @@ -18,7 +19,8 @@ import { data, dataWithoutPK } from '../shared/data'; IgxColumnComponent, IgxGridComponent, IgxCellTemplateDirective, - IgxButtonDirective + IgxButtonDirective, + IgxPaginatorComponent ] }) export class GridCellMergingComponent { From f23dd6cf4ed5915851605635c4c541d9397acdde Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 9 Jul 2025 14:51:01 +0300 Subject: [PATCH 012/106] chore(*): Fix row indexes in merged row area. --- .../igniteui-angular/src/lib/grids/grid/grid.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2f84f207d22..fdb8366e80b 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -50,7 +50,7 @@ @for (rowData of mergedData; track rowData; let rowIndex = $index) { - From f6ffa428065517c39a3168dd4260468c29463dce Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 9 Jul 2025 18:22:03 +0300 Subject: [PATCH 013/106] chore(*): Adjust some styles for row selection. --- .../core/styles/components/grid/_grid-theme.scss | 14 ++++++++++++++ .../grid-cellMerging.component.html | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index 47123b240b3..37d8794db01 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1825,6 +1825,20 @@ color: var-get($theme, 'row-selected-text-color'); background: var-get($theme, 'row-selected-background'); + &%grid-row--mrl { + background: transparent; + %grid-mrl-block { + color: var-get($theme, 'row-selected-text-color'); + background: var-get($theme, 'row-selected-background'); + %igx-grid__td--merged { + color: var-get($theme, 'row-selected-text-color'); + background: var-get($theme, 'row-selected-background'); + } + } + } + + + %grid-cell--selected, %grid-cell--pinned-selected { color: var-get($theme, 'cell-selected-within-text-color'); diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index da15f25881d..21d28741a49 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -1,5 +1,5 @@

Grid with cell merge

- +
From 4d1b981a518b1a430903b35c4d3c02029a8b4807 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 10 Jul 2025 16:15:28 +0300 Subject: [PATCH 014/106] chore(*): Merged cells navigation. --- .../src/lib/grids/grid-base.directive.ts | 18 +++ .../grids/grid-merge-navigation.service.ts | 108 ++++++++++++++++++ .../src/lib/grids/grid/grid.component.ts | 3 +- .../src/lib/grids/row.directive.ts | 6 + 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 projects/igniteui-angular/src/lib/grids/grid-merge-navigation.service.ts 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 44db042c792..e9d3ec5710c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -186,6 +186,7 @@ import { getCurrentResourceStrings } from '../core/i18n/resources'; import { isTree, recreateTree, recreateTreeFromFields } from '../data-operations/expressions-tree-util'; import { getUUID } from './common/random'; import { IGridMergeStrategy } from '../data-operations/merge-strategy'; +import { IgxGridMergeNavigationService } from './grid-merge-navigation.service'; interface IMatchInfoCache { row: any; @@ -3984,6 +3985,23 @@ export abstract class IgxGridBaseDirective implements GridType, if (this.actionStrip) { this.actionStrip.menuOverlaySettings.outlet = this.outlet; } + this._setupNavigationService(); + } + + protected _setupNavigationService() { + if (this.hasCellsToMerge) { + this.navigation = new IgxGridMergeNavigationService(this.platform); + this.navigation.grid = this; + } + } + + protected get hasCellsToMerge() { + const columnToMerge = this.visibleColumns.filter( + x => x.merge && (this.cellMergeMode ==='always' || + (this.cellMergeMode === 'onSort' && !!this.sortingExpressions + .find( x=> x.fieldName === x.fieldName))) + ); + return columnToMerge.length > 0; } /** diff --git a/projects/igniteui-angular/src/lib/grids/grid-merge-navigation.service.ts b/projects/igniteui-angular/src/lib/grids/grid-merge-navigation.service.ts new file mode 100644 index 00000000000..295f100a30d --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/grid-merge-navigation.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; +import { IgxGridNavigationService } from './grid-navigation.service'; +import { first } from 'rxjs/operators'; +/** @hidden */ +@Injectable() +export class IgxGridMergeNavigationService extends IgxGridNavigationService { + public override shouldPerformVerticalScroll(targetRowIndex: number, visibleColIndex: number): boolean { + const targetRec = this.grid.verticalScrollContainer.igxForOf[targetRowIndex]; + const field = this.grid.visibleColumns[visibleColIndex]?.field; + const rowSpan = targetRec?.cellMergeMeta?.get(field)?.rowSpan; + if (rowSpan > 1) { + const targetRow = super.getRowElementByIndex(targetRowIndex); + const containerHeight = this.grid.calcHeight ? Math.ceil(this.grid.calcHeight) : 0; + const scrollPos = this.getVerticalScrollPositions(targetRowIndex, rowSpan); + return (!targetRow || targetRow.offsetTop < Math.abs(this.containerTopOffset) + || containerHeight && containerHeight < scrollPos.rowBottom - Math.ceil(this.scrollTop)); + } else { + return super.shouldPerformVerticalScroll(targetRowIndex, visibleColIndex); + } + } + + public override performVerticalScrollToCell(rowIndex: number, visibleColIndex: number, cb?: () => void) { + const targetRec = this.grid.verticalScrollContainer.igxForOf[rowIndex]; + const field = this.grid.visibleColumns[visibleColIndex]?.field; + const rowSpan = targetRec?.cellMergeMeta?.get(field)?.rowSpan; + if (rowSpan > 1) { + const containerHeight = this.grid.calcHeight ? Math.ceil(this.grid.calcHeight) : 0; + const pos = this.getVerticalScrollPositions(rowIndex, rowSpan); + const row = super.getRowElementByIndex(rowIndex); + if ((this.scrollTop > pos.rowTop) && (!row || row.offsetTop < Math.abs(this.containerTopOffset))) { + if (pos.topOffset === 0) { + this.grid.verticalScrollContainer.scrollTo(rowIndex); + } else { + this.grid.verticalScrollContainer.scrollPosition = pos.rowTop; + } + } else { + this.grid.verticalScrollContainer.addScrollTop(Math.abs(pos.rowBottom - this.scrollTop - containerHeight)); + } + this.grid.verticalScrollContainer.chunkLoad + .pipe(first()).subscribe(() => { + if (cb) { + cb(); + } + }); + } else { + super.performVerticalScrollToCell(rowIndex, visibleColIndex, cb); + } + } + + protected override getNextPosition(rowIndex: number, colIndex: number, key: string, shift: boolean, ctrl: boolean, event: KeyboardEvent) { + + const field = this.grid.visibleColumns[colIndex]?.field; + switch (key) { + case 'tab': + case ' ': + case 'spacebar': + case 'space': + case 'escape': + case 'esc': + case 'enter': + case 'f2': + case 'left': + case 'arrowleft': + case 'arrowright': + case 'right': + // same as base for these keys + return super.getNextPosition(rowIndex, colIndex, key, shift, ctrl, event); + break; + case 'end': + rowIndex = ctrl ? this.findLastDataRowIndex() : this.activeNode.row; + colIndex = this.lastColumnIndex; + break; + case 'home': + rowIndex = ctrl ? this.findFirstDataRowIndex() : this.activeNode.row; + colIndex = 0; + break; + case 'arrowup': + case 'up': + const prevRec = this.grid.verticalScrollContainer.igxForOf[this.activeNode.row - 1]; + const root = prevRec?.cellMergeMeta?.get(field)?.root; + const prev = this.activeNode.row - (root?.cellMergeMeta?.get(field).rowSpan || 1); + colIndex = this.activeNode.column !== undefined ? this.activeNode.column : 0; + rowIndex = ctrl ? this.findFirstDataRowIndex() : prev; + break; + case 'arrowdown': + case 'down': + const rec = this.grid.verticalScrollContainer.igxForOf[this.activeNode.row]; + const next = this.activeNode.row + (rec?.cellMergeMeta?.get(field)?.rowSpan || 1); + colIndex = this.activeNode.column !== undefined ? this.activeNode.column : 0; + rowIndex = ctrl ? this.findLastDataRowIndex() : next; + break; + default: + return; + } + return { rowIndex, colIndex }; + } + + private getVerticalScrollPositions(rowIndex: number, rowSpan: number) { + const rowTop = this.grid.verticalScrollContainer.sizesCache[rowIndex]; + const rowBottom = this.grid.verticalScrollContainer.sizesCache[rowIndex + rowSpan]; + const topOffset = rowBottom - rowTop; + return { topOffset, rowTop, rowBottom }; + } + + private get scrollTop(): number { + return Math.abs(this.grid.verticalScrollContainer.getScroll().scrollTop); + } +} diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts index 8395d09cf5e..b3ea155bd0c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts @@ -1357,11 +1357,12 @@ export class IgxGridComponent extends IgxGridBaseDirective implements GridType, this._gridAPI.sort_groupBy_multiple(this._groupingExpressions); } - private _setupNavigationService() { + protected override _setupNavigationService() { if (this.hasColumnLayouts) { this.navigation = new IgxGridMRLNavigationService(this.platform); this.navigation.grid = this; } + super._setupNavigationService(); } private checkIfNoColumnField(expression: IGroupingExpression | Array | any): boolean { diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index d4e4b6ae5c6..1cdcacc5c3e 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -522,6 +522,12 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { public isCellActive(visibleColumnIndex) { const node = this.grid.navigation.activeNode; + const field = this.grid.visibleColumns[visibleColumnIndex].field; + const rowSpan = this.metaData?.cellMergeMeta?.get(field)?.rowSpan; + if (rowSpan > 1) { + return node ? (node.row >= this.index && node.row < this.index + rowSpan) + && node.column === visibleColumnIndex : false; + } return node ? node.row === this.index && node.column === visibleColumnIndex : false; } From aca57ee8a9a9d0fb09c394654daeb8bed62e6d52 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 10 Jul 2025 17:03:50 +0300 Subject: [PATCH 015/106] chore(*): Adjust for scenarios after horizontal nav into merged cell. --- .../src/lib/grids/grid-merge-navigation.service.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid-merge-navigation.service.ts b/projects/igniteui-angular/src/lib/grids/grid-merge-navigation.service.ts index 295f100a30d..870c26e34b5 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-merge-navigation.service.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-merge-navigation.service.ts @@ -48,8 +48,10 @@ export class IgxGridMergeNavigationService extends IgxGridNavigationService { } protected override getNextPosition(rowIndex: number, colIndex: number, key: string, shift: boolean, ctrl: boolean, event: KeyboardEvent) { - const field = this.grid.visibleColumns[colIndex]?.field; + const currentRec = this.grid.verticalScrollContainer.igxForOf[this.activeNode.row]; + const currentRootRec = currentRec?.cellMergeMeta?.get(field)?.root; + const currentIndex = currentRootRec ? this.grid.verticalScrollContainer.igxForOf.indexOf(currentRootRec) : this.activeNode.row; switch (key) { case 'tab': case ' ': @@ -76,16 +78,15 @@ export class IgxGridMergeNavigationService extends IgxGridNavigationService { break; case 'arrowup': case 'up': - const prevRec = this.grid.verticalScrollContainer.igxForOf[this.activeNode.row - 1]; - const root = prevRec?.cellMergeMeta?.get(field)?.root; - const prev = this.activeNode.row - (root?.cellMergeMeta?.get(field).rowSpan || 1); + const prevRec = this.grid.verticalScrollContainer.igxForOf[currentIndex - 1]; + const prevRoot = prevRec?.cellMergeMeta?.get(field)?.root; + const prev = currentIndex - (prevRoot?.cellMergeMeta?.get(field).rowSpan || 1); colIndex = this.activeNode.column !== undefined ? this.activeNode.column : 0; rowIndex = ctrl ? this.findFirstDataRowIndex() : prev; break; case 'arrowdown': case 'down': - const rec = this.grid.verticalScrollContainer.igxForOf[this.activeNode.row]; - const next = this.activeNode.row + (rec?.cellMergeMeta?.get(field)?.rowSpan || 1); + const next = currentIndex + ((currentRootRec || currentRec)?.cellMergeMeta?.get(field)?.rowSpan || 1); colIndex = this.activeNode.column !== undefined ? this.activeNode.column : 0; rowIndex = ctrl ? this.findLastDataRowIndex() : next; break; From e168c3a30ad05133a6eccc48a072bf2674fbeb08 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 10 Jul 2025 18:42:05 +0300 Subject: [PATCH 016/106] chore(*): Add integration with pinning. --- .../src/lib/data-operations/merge-strategy.ts | 10 ++++++++++ .../src/lib/grids/grid-base.directive.ts | 6 +++++- .../src/lib/grids/grid/grid.component.ts | 2 +- .../igniteui-angular/src/lib/grids/row.directive.ts | 6 ++++-- .../grid-cellMerging/grid-cellMerging.component.html | 5 ++++- src/app/grid-cellMerging/grid-cellMerging.component.ts | 6 +++++- 6 files changed, 29 insertions(+), 6 deletions(-) diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index c9afd6fc3f9..183b29ed4c6 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -31,7 +31,17 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { let prev = null; let index = 0; for (const rec of data) { + const recData = result[index]; + // if this is some special record type - add and skip merging + if (rec.ghostRecord) { + if(!recData) { + result.push(rec); + } + prev = null; + index++; + continue; + } let recToUpdateData = recData ?? { recordRef: rec, cellMergeMeta: new Map() }; recToUpdateData.cellMergeMeta.set(field, { rowSpan: 1 }); if (prev && comparer(prev.recordRef, recToUpdateData.recordRef, field)) { 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 e9d3ec5710c..a2a5116ce34 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3668,7 +3668,11 @@ export abstract class IgxGridBaseDirective implements GridType, public getMergeCellOffset(rec) { const index = this.verticalScrollContainer.igxForOf.indexOf(rec); - return -(this.verticalScrollContainer.scrollPosition - this.verticalScrollContainer.getScrollForIndex(index)); + let offset = this.verticalScrollContainer.scrollPosition - this.verticalScrollContainer.getScrollForIndex(index); + if (this.hasPinnedRecords && this.isRowPinningToTop) { + offset -= this.pinnedRowHeight; + } + return -offset; } /** diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts index b3ea155bd0c..e48438fad91 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts @@ -967,7 +967,7 @@ export class IgxGridComponent extends IgxGridBaseDirective implements GridType, id: null }, disabled: this.isGhostRecord(rowData), - metaData: rowData + metaData: this.isRecordMerged(rowData) ? rowData : null }; } diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index 1cdcacc5c3e..94df28fb9f0 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -611,7 +611,8 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { protected getMergeCellSpan(col: ColumnType){ const rowCount = this.metaData.cellMergeMeta.get(col.field).rowSpan; let sizeSpans = ""; - for (let index = this.index; index < this.index + rowCount; index++) { + const indexInData = this.grid.verticalScrollContainer.igxForOf.indexOf(this.metaData); + for (let index = indexInData; index < indexInData + rowCount; index++) { const size = this.grid.verticalScrollContainer.getSizeAt(index); sizeSpans += size + 'px '; } @@ -619,7 +620,8 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { } protected getRowHeight() { - const size = this.grid.verticalScrollContainer.getSizeAt(this.index) - 1; + const indexInData = this.grid.verticalScrollContainer.igxForOf.indexOf(this.metaData); + const size = this.grid.verticalScrollContainer.getSizeAt(indexInData) - 1; return size || this.grid.rowHeight; } diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 21d28741a49..a98f0885a7a 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -3,7 +3,7 @@

Grid with cell merge

- + - - + - - + diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.ts index 69695e21ee2..ea8a0a04543 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 @@ -50,7 +50,7 @@ import { IgxGridValidationService } from '../grid/grid-validation.service'; import { IgxGridHierarchicalPipe, IgxGridHierarchicalPagingPipe } from './hierarchical-grid.pipes'; import { IgxSummaryDataPipe } from '../summaries/grid-root-summary.pipe'; import { IgxGridTransactionPipe, IgxHasVisibleColumnsPipe, IgxGridRowPinningPipe, IgxGridAddRowPipe, IgxGridRowClassesPipe, IgxGridRowStylesPipe, IgxStringReplacePipe } from '../common/pipes'; -import { IgxGridSortingPipe, IgxGridFilteringPipe } from '../grid/grid.pipes'; +import { IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe } from '../grid/grid.pipes'; import { IgxGridColumnResizerComponent } from '../resizing/resizer.component'; import { IgxRowEditTabStopDirective } from '../grid.rowEdit.directive'; import { IgxIconComponent } from '../../icon/icon.component'; @@ -350,7 +350,8 @@ export class IgxChildGridRowComponent implements AfterViewInit, OnInit { IgxSummaryDataPipe, IgxGridHierarchicalPipe, IgxGridHierarchicalPagingPipe, - IgxStringReplacePipe + IgxStringReplacePipe, + IgxGridCellMergePipe ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) @@ -942,7 +943,7 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti /** * @hidden */ - public isChildGridRecord(record: any): boolean { + public override isChildGridRecord(record: any): boolean { // Can be null when there is defined layout but no child data was found return record?.childGridsData !== undefined; } @@ -986,13 +987,14 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti } } else { return { - $implicit: this.isGhostRecord(rowData) ? rowData.recordRef : rowData, + $implicit: this.isGhostRecord(rowData) || this.isRecordMerged(rowData) ? rowData.recordRef : rowData, templateID: { type: 'dataRow', id: null }, index: this.getDataViewIndex(rowIndex, pinned), - disabled: this.isGhostRecord(rowData) + disabled: this.isGhostRecord(rowData), + metaData: this.isRecordMerged(rowData) ? rowData : null }; } } diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html index 42d4a19c5d3..4c286034977 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html @@ -54,32 +54,19 @@ } - - + @if (this.hasMergedCells) { +
+ +
+ } + @else { + + }
@if (pinnedColumns.length > 0 && !grid.isPinningToStart) { @@ -132,3 +119,35 @@ } + + + + + diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 0be130b991f..f73e2c33de8 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -1,15 +1,16 @@

Grid with cell merge

- + -
- - -
+
@@ -33,3 +34,31 @@

Grid with cell merge

+ +

Hierarchical grid with cell merge

+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index f60a23cd7a9..e44ce6a7179 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -12,8 +12,11 @@ import { IgxGridToolbarExporterComponent, IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent, + IgxHierarchicalGridComponent, IgxPaginatorComponent, + IgxRowIslandComponent, } from 'igniteui-angular'; +import { HIERARCHICAL_DATA } from '../shared/hierarchicalData'; import { data, dataWithoutPK } from '../shared/data'; @@ -34,10 +37,13 @@ import { data, dataWithoutPK } from '../shared/data'; IgxGridToolbarActionsComponent, IgxGridToolbarPinningComponent, IgxGridToolbarHidingComponent, - IgxGridToolbarExporterComponent + IgxGridToolbarExporterComponent, + IgxHierarchicalGridComponent, + IgxRowIslandComponent ] }) export class GridCellMergingComponent { + public hierarchicalData = HIERARCHICAL_DATA.concat(HIERARCHICAL_DATA).concat(HIERARCHICAL_DATA); public data = [{ ProductID: 1, ProductName: 'Chai', From 5c4587fb89e879df1e0e46e33c3ed66696fafaa4 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 16 Jul 2025 12:45:33 +0300 Subject: [PATCH 029/106] chore(*): Implement for tree grid. Add 2 strategies. --- .../src/lib/data-operations/merge-strategy.ts | 41 ++++ .../src/lib/grids/grid-base.directive.ts | 13 +- .../tree-grid/tree-grid-row.component.html | 223 ++++++++---------- .../grids/tree-grid/tree-grid.component.html | 26 +- .../grids/tree-grid/tree-grid.component.ts | 11 +- .../grid-cellMerging.component.html | 13 +- .../grid-cellMerging.component.ts | 6 +- 7 files changed, 194 insertions(+), 139 deletions(-) diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index d18a6c43be5..4d20587775e 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -79,3 +79,44 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { return a === b; } } + + +export class DefaultTreeGridMergeStrategy extends DefaultMergeStrategy { + /* blazorSuppress */ + public override comparer(prevRecord: any, record: any, field: string): boolean { + const a = prevRecord.data[field]; + const b = record.data[field]; + const an = (a === null || a === undefined); + const bn = (b === null || b === undefined); + if (an) { + if (bn) { + return true; + } + return false; + } else if (bn) { + return false; + } + return a === b; + } +} + +export class ByLevelTreeGridMergeStrategy extends DefaultMergeStrategy { + /* blazorSuppress */ + public override comparer(prevRecord: any, record: any, field: string): boolean { + const a = prevRecord.data[field]; + const b = record.data[field]; + const levelA = prevRecord.level; + const levelB = record.level; + const an = (a === null || a === undefined); + const bn = (b === null || b === undefined); + if (an) { + if (bn) { + return true; + } + return false; + } else if (bn) { + return false; + } + return a === b && levelA === levelB; + } +} 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 a453a3af36c..97a625a806e 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -185,7 +185,7 @@ import { IgxGridValidationService } from './grid/grid-validation.service'; import { getCurrentResourceStrings } from '../core/i18n/resources'; import { isTree, recreateTree, recreateTreeFromFields } from '../data-operations/expressions-tree-util'; import { getUUID } from './common/random'; -import { IGridMergeStrategy } from '../data-operations/merge-strategy'; +import { DefaultMergeStrategy, IGridMergeStrategy } from '../data-operations/merge-strategy'; interface IMatchInfoCache { row: any; @@ -2505,7 +2505,12 @@ export abstract class IgxGridBaseDirective implements GridType, * ``` */ @Input() - public mergeStrategy: IGridMergeStrategy; + get mergeStrategy() { + return this._mergeStrategy; + } + set mergeStrategy(value) { + this._mergeStrategy = value; + } /** * Gets/Sets the sorting options - single or multiple sorting. @@ -3181,6 +3186,10 @@ export abstract class IgxGridBaseDirective implements GridType, protected _columnPinning = false; protected _pinnedRecordIDs = []; + /** + * @hidden + */ + protected _mergeStrategy: IGridMergeStrategy = new DefaultMergeStrategy(); /** * @hidden diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html index 05bbb9e49c4..fd1fa6737fb 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html @@ -23,66 +23,19 @@ } - - - - - - - - - + @if (this.hasMergedCells) { +
+ +
+ } + @else { + + }
@if (pinnedColumns.length > 0 && !grid.isPinningToStart) { @@ -106,69 +59,91 @@ @for (col of pinnedColumns | igxNotGrouped; track trackPinnedColumn(col)) { - - - - - - - - - + @if (this.hasMergedCells) { +
+ +
+ } + @else { + + } }
+ + + + + + + + + + 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 5db61b5629c..3560107c5ce 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 @@ -30,6 +30,14 @@ [igxColumnMovingDrop]="headerContainer" [attr.droppable]="true" id="left" class="igx-grid__scroll-on-drag-pinned" [style.left.px]="pinnedWidth"> } + + @for (rowData of mergedDataInView; track rowData.record;) { + + + } + @if (data | treeGridTransaction:pipeTrigger @@ -38,7 +46,8 @@ | treeGridAddRow:true:pipeTrigger | gridRowPinning:id:true:pipeTrigger | treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true - | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true; as pinnedData + | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true + | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndex:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
@@ -76,15 +86,15 @@ - - + - - + diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts index 30f413e00e4..32a424a37a7 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts @@ -82,6 +82,8 @@ import { IgxGridDragSelectDirective } from '../selection/drag-select.directive'; import { IgxGridBodyDirective } from '../grid.common'; import { IgxGridHeaderRowComponent } from '../headers/grid-header-row.component'; import { IgxTextHighlightService } from '../../directives/text-highlight/text-highlight.service'; +import { IgxGridCellMergePipe } from '../grid/grid.pipes'; +import { DefaultTreeGridMergeStrategy, IGridMergeStrategy } from '../../data-operations/merge-strategy'; let NEXT_ID = 0; @@ -168,7 +170,8 @@ let NEXT_ID = 0; IgxTreeGridSummaryPipe, IgxTreeGridNormalizeRecordsPipe, IgxTreeGridAddRowPipe, - IgxStringReplacePipe + IgxStringReplacePipe, + IgxGridCellMergePipe ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) @@ -348,6 +351,7 @@ export class IgxTreeGridComponent extends IgxGridBaseDirective implements GridTy protected override _filterStrategy = new TreeGridFilteringStrategy(); protected override _transactions: HierarchicalTransactionService; + protected override _mergeStrategy: IGridMergeStrategy = new DefaultTreeGridMergeStrategy(); private _data; private _rowLoadingIndicatorTemplate: TemplateRef; private _expansionDepth = Infinity; @@ -699,13 +703,14 @@ export class IgxTreeGridComponent extends IgxGridBaseDirective implements GridTy */ public getContext(rowData: any, rowIndex: number, pinned?: boolean): any { return { - $implicit: this.isGhostRecord(rowData) ? rowData.recordRef : rowData, + $implicit: this.isGhostRecord(rowData) || this.isRecordMerged(rowData) ? rowData.recordRef : rowData, index: this.getDataViewIndex(rowIndex, pinned), templateID: { type: this.isSummaryRow(rowData) ? 'summaryRow' : 'dataRow', id: null }, - disabled: this.isGhostRecord(rowData) ? rowData.recordRef.isFilteredOutParent === undefined : false + disabled: this.isGhostRecord(rowData) ? rowData.recordRef.isFilteredOutParent === undefined : false, + metaData: this.isRecordMerged(rowData) ? rowData : null }; } diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index f73e2c33de8..5dd64944ae1 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -37,7 +37,7 @@

Grid with cell merge

Hierarchical grid with cell merge

- @@ -62,3 +62,14 @@

Hierarchical grid with cell merge

+ +

Tree grid with cell merge

+ + + + + + + + diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index e44ce6a7179..19acbca30da 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -15,10 +15,12 @@ import { IgxHierarchicalGridComponent, IgxPaginatorComponent, IgxRowIslandComponent, + IgxTreeGridComponent } from 'igniteui-angular'; import { HIERARCHICAL_DATA } from '../shared/hierarchicalData'; import { data, dataWithoutPK } from '../shared/data'; +import { HIERARCHICAL_SAMPLE_DATA } from '../shared/sample-data'; @Component({ selector: 'app-grid-cellMerging', @@ -39,11 +41,13 @@ import { data, dataWithoutPK } from '../shared/data'; IgxGridToolbarHidingComponent, IgxGridToolbarExporterComponent, IgxHierarchicalGridComponent, - IgxRowIslandComponent + IgxRowIslandComponent, + IgxTreeGridComponent ] }) export class GridCellMergingComponent { public hierarchicalData = HIERARCHICAL_DATA.concat(HIERARCHICAL_DATA).concat(HIERARCHICAL_DATA); + public treeData = HIERARCHICAL_SAMPLE_DATA; public data = [{ ProductID: 1, ProductName: 'Chai', From 7ac17e45e2ef5803cb0ec4cdfe903fceb291ea11 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 16 Jul 2025 14:39:55 +0300 Subject: [PATCH 030/106] chore(*): Export strategies in package. --- projects/igniteui-angular-elements/src/public_api.ts | 3 ++- projects/igniteui-angular/src/public_api.ts | 1 + src/app/grid-cellMerging/grid-cellMerging.component.html | 2 +- src/app/grid-cellMerging/grid-cellMerging.component.ts | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular-elements/src/public_api.ts b/projects/igniteui-angular-elements/src/public_api.ts index d8081637986..aa796e8c5b2 100644 --- a/projects/igniteui-angular-elements/src/public_api.ts +++ b/projects/igniteui-angular-elements/src/public_api.ts @@ -12,7 +12,7 @@ import { IgxPivotDateDimension } from 'projects/igniteui-angular/src/lib/grids/p import { PivotDimensionType } from 'projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.interface'; import { IgxDateSummaryOperand, IgxNumberSummaryOperand, IgxSummaryOperand, IgxTimeSummaryOperand } from 'projects/igniteui-angular/src/lib/grids/summaries/grid-summary'; import { HorizontalAlignment, VerticalAlignment } from 'projects/igniteui-angular/src/lib/services/overlay/utilities'; - +import { ByLevelTreeGridMergeStrategy } from 'projects/igniteui-angular/src/lib/data-operations/merge-strategy'; /** Export Public API, TODO: reorganize, Generate all w/ renames? */ export { @@ -35,6 +35,7 @@ export { NoopSortingStrategy as IgcNoopSortingStrategy, NoopFilteringStrategy as IgcNoopFilteringStrategy, + ByLevelTreeGridMergeStrategy as IgcByLevelTreeGridMergeStrategy, // Pivot API IgxPivotDateDimension as IgcPivotDateDimension, diff --git a/projects/igniteui-angular/src/public_api.ts b/projects/igniteui-angular/src/public_api.ts index 0e844761ba2..d4a70104a95 100644 --- a/projects/igniteui-angular/src/public_api.ts +++ b/projects/igniteui-angular/src/public_api.ts @@ -52,6 +52,7 @@ export * from './lib/data-operations/filtering-expressions-tree'; export * from './lib/data-operations/filtering-condition'; export * from './lib/data-operations/filtering-state.interface'; export * from './lib/data-operations/filtering-strategy'; +export * from './lib/data-operations/merge-strategy'; export { ExpressionsTreeUtil } from './lib/data-operations/expressions-tree-util'; export * from './lib/data-operations/groupby-expand-state.interface'; export * from './lib/data-operations/groupby-record.interface'; diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 5dd64944ae1..f9c41c1580e 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -65,7 +65,7 @@

Hierarchical grid with cell merge

Tree grid with cell merge

- diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index 19acbca30da..497e4001350 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -21,6 +21,7 @@ import { HIERARCHICAL_DATA } from '../shared/hierarchicalData'; import { data, dataWithoutPK } from '../shared/data'; import { HIERARCHICAL_SAMPLE_DATA } from '../shared/sample-data'; +import { ByLevelTreeGridMergeStrategy } from 'igniteui-angular'; @Component({ selector: 'app-grid-cellMerging', @@ -48,6 +49,7 @@ import { HIERARCHICAL_SAMPLE_DATA } from '../shared/sample-data'; export class GridCellMergingComponent { public hierarchicalData = HIERARCHICAL_DATA.concat(HIERARCHICAL_DATA).concat(HIERARCHICAL_DATA); public treeData = HIERARCHICAL_SAMPLE_DATA; + public treeGridMergeStrategy = new ByLevelTreeGridMergeStrategy(); public data = [{ ProductID: 1, ProductName: 'Chai', From 401370e6e9b4c0ba1e70662ff498724efedf3ae4 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 16 Jul 2025 16:39:25 +0300 Subject: [PATCH 031/106] chore(*): Fix chip displaying when in merged cell. --- .../src/lib/grids/grid/grid-row.component.html | 8 ++++---- .../hierarchical-grid/hierarchical-row.component.html | 4 ++-- .../src/lib/grids/pivot-grid/pivot-row.component.html | 2 +- projects/igniteui-angular/src/lib/grids/row.directive.ts | 4 ++-- .../src/lib/grids/tree-grid/tree-grid-row.component.html | 4 ++-- src/app/grid-cellMerging/grid-cellMerging.component.html | 1 - src/app/grid-cellMerging/grid-cellMerging.component.ts | 2 ++ 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index b8e27a76e34..e8f2a00cab7 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -162,7 +162,7 @@ [lastSearchInfo]="grid.lastSearchInfo" [active]="isCellActive(col.visibleIndex)" [cellSelectionMode]="grid.cellSelection" - [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)" + [displayPinnedChip]="shouldDisplayPinnedChip(col)" #cell> @@ -194,7 +194,7 @@ [lastSearchInfo]="grid.lastSearchInfo" [active]="isCellActive(col.visibleIndex)" [cellSelectionMode]="grid.cellSelection" - [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)" + [displayPinnedChip]="shouldDisplayPinnedChip(col)" #cell> @@ -226,7 +226,7 @@ [lastSearchInfo]="grid.lastSearchInfo" [active]="isCellActive(col.visibleIndex)" [cellSelectionMode]="grid.cellSelection" - [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)" + [displayPinnedChip]="shouldDisplayPinnedChip(col)" #cell> @@ -258,7 +258,7 @@ [lastSearchInfo]="grid.lastSearchInfo" [active]="isCellActive(col.visibleIndex)" [cellSelectionMode]="grid.cellSelection" - [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)" + [displayPinnedChip]="shouldDisplayPinnedChip(col)" #cell> diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html index 4c286034977..1f0b9e60507 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html @@ -114,7 +114,7 @@ [cellValidationErrorTemplate]="col.errorTemplate" [lastSearchInfo]="grid.lastSearchInfo" [cellSelectionMode]="grid.cellSelection" - [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)"> + [displayPinnedChip]="shouldDisplayPinnedChip(col)"> } @@ -148,6 +148,6 @@ [cellValidationErrorTemplate]="col.errorTemplate" [lastSearchInfo]="grid.lastSearchInfo" [cellSelectionMode]="grid.cellSelection" - [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)"> + [displayPinnedChip]="shouldDisplayPinnedChip(col)"> diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row.component.html b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row.component.html index 39d6a48bd63..a6a93da2e92 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-row.component.html @@ -16,7 +16,7 @@ [style.flex-basis]="col.resolvedWidth" [width]="col.getCellWidth()" [visibleColumnIndex]="col.visibleIndex" [value]="pivotAggregationData[col.field] | dataMapper:col.field:grid.pipeTrigger:pivotAggregationData[col.field]:col.hasNestedPath" [cellTemplate]="col.bodyTemplate" [lastSearchInfo]="grid.lastSearchInfo" - [cellSelectionMode]="grid.cellSelection" [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)" + [cellSelectionMode]="grid.cellSelection" [displayPinnedChip]="shouldDisplayPinnedChip(col)" (pointerdown)="grid.navigation.focusOutRowHeader($event)"> diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index f094531c37f..4794d494075 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -578,8 +578,8 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { /** * @hidden */ - public shouldDisplayPinnedChip(visibleColumnIndex: number): boolean { - return this.pinned && this.disabled && visibleColumnIndex === 0; + public shouldDisplayPinnedChip(col: ColumnType): boolean { + return this.pinned && this.disabled && col.visibleIndex === 0 && !this.metaData?.cellMergeMeta?.get(col.field)?.root; } /** diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html index fd1fa6737fb..16ccc6ae995 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html @@ -105,7 +105,7 @@ [lastSearchInfo]="grid.lastSearchInfo" [active]="isCellActive(col.visibleIndex)" [cellSelectionMode]="grid.cellSelection" - [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)" + [displayPinnedChip]="shouldDisplayPinnedChip(col)" #treeCell> @@ -142,7 +142,7 @@ [lastSearchInfo]="grid.lastSearchInfo" [active]="isCellActive(col.visibleIndex)" [cellSelectionMode]="grid.cellSelection" - [displayPinnedChip]="shouldDisplayPinnedChip(col.visibleIndex)" + [displayPinnedChip]="shouldDisplayPinnedChip(col)" #treeCell> diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index f9c41c1580e..e2054dac1a7 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -59,7 +59,6 @@

Hierarchical grid with cell merge

- diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index 497e4001350..ec6eed2b1a3 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -50,6 +50,8 @@ export class GridCellMergingComponent { public hierarchicalData = HIERARCHICAL_DATA.concat(HIERARCHICAL_DATA).concat(HIERARCHICAL_DATA); public treeData = HIERARCHICAL_SAMPLE_DATA; public treeGridMergeStrategy = new ByLevelTreeGridMergeStrategy(); + public alignBottom = { alignItems: "flex-end", paddingBottom: "12px"}; + public alignTop= { alignItems: "flex-start", paddingTop: "12px" }; public data = [{ ProductID: 1, ProductName: 'Chai', From 5e1b702f619c4b3d9313df1309a3dc920749916d Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 16 Jul 2025 17:16:15 +0300 Subject: [PATCH 032/106] chore(*): Minor tweaks. Console warn on invalid setup. --- .../src/lib/data-operations/merge-strategy.ts | 1 + .../src/lib/grids/columns/column.component.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index 4d20587775e..1c16f7df755 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -14,6 +14,7 @@ export interface IGridMergeStrategy { activeRowIndex? : number, grid?: GridType ) => any[]; + comparer: (prevRecord: any, record: any, field: string) => boolean; } export class DefaultMergeStrategy implements IGridMergeStrategy { diff --git a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts index 58b3de78586..7aa2e7807b7 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts @@ -115,7 +115,17 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy * */ @Input() - public merge = false; + public get merge() { + return this._merge; + } + + public set merge(value) { + if (this.grid.hasColumnLayouts) { + console.warn('Merging is not supported with multi-row layouts.'); + return; + } + this._merge = value; + } /** * @hidden @internal @@ -1911,6 +1921,10 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy * @hidden */ protected _groupable = false; + /** + * @hidden + */ + protected _merge = false; /** * @hidden */ From 58e6bfac1c39e6b505efeb13965233bec2b9d140 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 17 Jul 2025 13:39:30 +0300 Subject: [PATCH 033/106] chore(*): Fix lint errors. --- .../igniteui-angular/src/lib/grids/grid-base.directive.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 97a625a806e..325f9db900f 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -2505,10 +2505,10 @@ export abstract class IgxGridBaseDirective implements GridType, * ``` */ @Input() - get mergeStrategy() { + public get mergeStrategy() { return this._mergeStrategy; } - set mergeStrategy(value) { + public set mergeStrategy(value) { this._mergeStrategy = value; } @@ -3974,7 +3974,7 @@ export abstract class IgxGridBaseDirective implements GridType, public get columnsToMerge() : ColumnType[] { return this.visibleColumns.filter( x => x.merge && (this.cellMergeMode ==='always' || - (this.cellMergeMode === 'onSort' && !!this.sortingExpressions.find( x=> x.fieldName === x.fieldName))) + (this.cellMergeMode === 'onSort' && !!this.sortingExpressions.find( y => y.fieldName === x.field))) ); } @@ -4058,7 +4058,7 @@ export abstract class IgxGridBaseDirective implements GridType, const columnToMerge = this.visibleColumns.filter( x => x.merge && (this.cellMergeMode ==='always' || (this.cellMergeMode === 'onSort' && !!this.sortingExpressions - .find( x=> x.fieldName === x.fieldName))) + .find(y => y.fieldName === x.field))) ); return columnToMerge.length > 0; } From 5e81bf91320d989045f9f13e7f4adb8e8904fa20 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 17 Jul 2025 13:44:02 +0300 Subject: [PATCH 034/106] chore(*): Fix theming lint. --- .../src/lib/core/styles/components/grid/_grid-theme.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index bf9d1774dd5..605fcda9a59 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1830,10 +1830,12 @@ %igx-grid__td--merged { color: var-get($theme, 'row-selected-text-color'); background: var-get($theme, 'row-selected-background'); + &:hover { background: var-get($theme, 'row-selected-hover-background'); color: var-get($theme, 'row-selected-hover-text-color'); } + &%igx-grid__td--merged-hovered { background: var-get($theme, 'row-selected-hover-background'); color: var-get($theme, 'row-selected-hover-text-color'); From b2b61a7492cdcb6f072cabef5cd8bd95d425aff8 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 17 Jul 2025 15:02:18 +0300 Subject: [PATCH 035/106] chore(*): Fix build and imports. --- .../src/lib/data-operations/data-util.ts | 7 ------- .../src/lib/data-operations/merge-strategy.ts | 13 +++++++++---- .../src/lib/grids/grid/grid.pipes.ts | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) 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 429321ffbef..bdd51b5d07f 100644 --- a/projects/igniteui-angular/src/lib/data-operations/data-util.ts +++ b/projects/igniteui-angular/src/lib/data-operations/data-util.ts @@ -38,13 +38,6 @@ import { DefaultMergeStrategy, IGridMergeStrategy } from './merge-strategy'; } as const; export type DataType = (typeof DataType)[keyof typeof DataType]; - -export interface IMergeByResult { - rowSpan: number; - root?: any; - prev?: any; -} - /** * @hidden */ diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index 1c16f7df755..6339f0064d9 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -1,9 +1,14 @@ -import { GridCellMergeMode, IMergeByResult } from 'igniteui-angular'; -import type { KeyOfOrString } from '../core/types'; -import { IBaseEventArgs } from '../core/utils'; -import { ColumnType, GridType } from '../grids/common/grid.interface'; +import { GridType } from '../grids/common/grid.interface'; + + +export interface IMergeByResult { + rowSpan: number; + root?: any; + prev?: any; +} + export interface IGridMergeStrategy { /* blazorSuppress */ merge: ( diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index dbfb9d676c1..8cef2a8034e 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -9,7 +9,7 @@ import { ColumnType, GridType, IGX_GRID_BASE } from '../common/grid.interface'; import { FilterUtil, IFilteringStrategy } from '../../data-operations/filtering-strategy'; import { ISortingExpression } from '../../data-operations/sorting-strategy'; import { IGridSortingStrategy, IGridGroupingStrategy } from '../common/strategy'; -import { GridCellMergeMode } from 'igniteui-angular'; +import { GridCellMergeMode } from '../common/enums'; /** * @hidden From 9a7a2d25bfe2ed796ee5b19aa8c8395a4d996c07 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 17 Jul 2025 15:13:16 +0300 Subject: [PATCH 036/106] chore(*): Add null check. --- projects/igniteui-angular/src/lib/grids/row.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index 4794d494075..0e6c12ef6b7 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -524,7 +524,7 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { public isCellActive(visibleColumnIndex) { const node = this.grid.navigation.activeNode; - const field = this.grid.visibleColumns[visibleColumnIndex].field; + const field = this.grid.visibleColumns[visibleColumnIndex]?.field; const rowSpan = this.metaData?.cellMergeMeta?.get(field)?.rowSpan; if (rowSpan > 1) { return node ? (node.row >= this.index && node.row < this.index + rowSpan) From ce0c854c386590ac1321c20730cd2eee87821ea8 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 17 Jul 2025 16:53:59 +0300 Subject: [PATCH 037/106] chore(*): Null check for activeRowIndex. --- projects/igniteui-angular/src/lib/grids/grid-base.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 325f9db900f..13c24649a0b 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -4051,7 +4051,7 @@ export abstract class IgxGridBaseDirective implements GridType, protected get activeRowIndex() { - return this.navigation.activeNode.row; + return this.navigation.activeNode?.row; } protected get hasCellsToMerge() { From a6fc511f5ab346aad330b17267bdb555307f41b8 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 17 Jul 2025 17:07:49 +0300 Subject: [PATCH 038/106] chore(*): Generate elements config. --- .../src/analyzer/elements.config.ts | 3 +++ .../igniteui-angular/src/lib/grids/grid-base.directive.ts | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts index be2f3f4d6d5..e78d3492d9d 100644 --- a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts +++ b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts @@ -95,6 +95,7 @@ export var registerConfig = [ ], numericProps: ["rowEnd", "colEnd", "rowStart", "colStart"], boolProps: [ + "merge", "sortable", "selectable", "groupable", @@ -158,6 +159,7 @@ export var registerConfig = [ "expanded", "searchable", "hidden", + "merge", "sortable", "groupable", "editable", @@ -213,6 +215,7 @@ export var registerConfig = [ "collapsible", "expanded", "searchable", + "merge", "sortable", "groupable", "editable", 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 13c24649a0b..8b34b98060e 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3695,7 +3695,7 @@ export abstract class IgxGridBaseDirective implements GridType, return rec?.cellMergeMeta; } - public getMergeCellOffset(rowData) { + protected getMergeCellOffset(rowData) { const index = rowData.index; let offset = this.verticalScrollContainer.scrollPosition - this.verticalScrollContainer.getScrollForIndex(index); if (this.hasPinnedRecords && this.isRowPinningToTop) { @@ -3971,6 +3971,10 @@ export abstract class IgxGridBaseDirective implements GridType, } } + /** + * @hidden + * @internal + */ public get columnsToMerge() : ColumnType[] { return this.visibleColumns.filter( x => x.merge && (this.cellMergeMode ==='always' || From d0254734c210cabbedbfe65d02e5e5d5f6ca803c Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 17 Jul 2025 18:03:04 +0300 Subject: [PATCH 039/106] chore(*): Break up merge groups on cell selection. --- .../igniteui-angular/src/lib/data-operations/data-util.ts | 4 ++-- .../src/lib/data-operations/merge-strategy.ts | 6 +++--- .../igniteui-angular/src/lib/grids/grid-base.directive.ts | 6 ++++-- .../igniteui-angular/src/lib/grids/grid/grid.component.html | 4 ++-- projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 4 ++-- .../hierarchical-grid/hierarchical-grid.component.html | 4 ++-- .../src/lib/grids/tree-grid/tree-grid.component.html | 4 ++-- src/app/grid-cellMerging/grid-cellMerging.component.html | 2 +- 8 files changed, 18 insertions(+), 16 deletions(-) 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 bdd51b5d07f..0066cd58f4e 100644 --- a/projects/igniteui-angular/src/lib/data-operations/data-util.ts +++ b/projects/igniteui-angular/src/lib/data-operations/data-util.ts @@ -91,11 +91,11 @@ export class DataUtil { return grouping.groupBy(data, state, grid, groupsRecords, fullResult); } - public static merge(data: T[], columns: ColumnType[], strategy: IGridMergeStrategy = new DefaultMergeStrategy(), activeRowIndex = -1, grid: GridType = null, + public static merge(data: T[], columns: ColumnType[], strategy: IGridMergeStrategy = new DefaultMergeStrategy(), activeRowIndexes = [], grid: GridType = null, ): any[] { let result = []; for (const col of columns) { - strategy.merge(data, col.field, col.mergingComparer, result, activeRowIndex, grid); + strategy.merge(data, col.field, col.mergingComparer, result, activeRowIndexes, grid); } return result; } diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index 6339f0064d9..d1e5b5f9b5a 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -16,7 +16,7 @@ export interface IGridMergeStrategy { field: string, comparer: (prevRecord: any, currentRecord: any, field: string) => boolean, result: any[], - activeRowIndex? : number, + activeRowIndexes : number[], grid?: GridType ) => any[]; comparer: (prevRecord: any, record: any, field: string) => boolean; @@ -35,7 +35,7 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { field: string, comparer: (prevRecord: any, record: any, field: string) => boolean = this.comparer, result: any[], - activeRowIndex?: number, + activeRowIndexes : number[], grid?: GridType ) { let prev = null; @@ -44,7 +44,7 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { const recData = result[index]; // if this is active row or some special record type - add and skip merging - if (activeRowIndex === index || (grid && grid.isDetailRecord(rec) || grid.isGroupByRecord(rec) || grid.isChildGridRecord(rec))) { + if (activeRowIndexes.indexOf(index) != -1 || (grid && grid.isDetailRecord(rec) || grid.isGroupByRecord(rec) || grid.isChildGridRecord(rec))) { if(!recData) { result.push(rec); } 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 8b34b98060e..b41b24eb7bb 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -4054,8 +4054,10 @@ export abstract class IgxGridBaseDirective implements GridType, } - protected get activeRowIndex() { - return this.navigation.activeNode?.row; + protected get activeRowIndexes(): number[] { + const activeRow = this.navigation.activeNode?.row; + const selectedCellIndexes = (this.selectionService.selection?.keys() as any)?.toArray(); + return [activeRow, ...selectedCellIndexes]; } protected get hasCellsToMerge() { 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 04915e5f558..4e86643da62 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -63,7 +63,7 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:groupingExpressions:sortStrategy:id:pipeTrigger:true - | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndex:pipeTrigger; as pinnedData) { + | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:pipeTrigger; as pinnedData) { @if (pinnedData.length > 0) {
0) {
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 3560107c5ce..9e16f82611d 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 @@ -47,7 +47,7 @@ | gridRowPinning:id:true:pipeTrigger | treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true - | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndex:pipeTrigger; as pinnedData + | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index e2054dac1a7..15a44d78af3 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -1,5 +1,5 @@

Grid with cell merge

- From 190f29d52afa0635f30619ad87a17f9caad249da Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 18 Jul 2025 11:58:07 +0300 Subject: [PATCH 040/106] chore(*): Update active indexes on events. Cache result to limit pipe trigger. --- .../src/lib/grids/grid-base.directive.ts | 20 ++++++++++++++++--- .../lib/grids/selection/selection.service.ts | 9 ++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) 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 b41b24eb7bb..43cd98538a7 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3246,6 +3246,7 @@ export abstract class IgxGridBaseDirective implements GridType, private _filteredSortedData = null; private _filteredData = null; private _mergedDataInView = null; + private _activeRowIndexes = null; private _customDragIndicatorIconTemplate: TemplateRef; private _excelStyleHeaderIconTemplate: TemplateRef; @@ -3907,6 +3908,14 @@ export abstract class IgxGridBaseDirective implements GridType, this.autoSizeColumnsInView(); this._firstAutoResize = false; }); + + this.activeNodeChange.pipe(filter(() => !this._init), destructor).subscribe(() => { + this._activeRowIndexes = null; + }); + + this.selectionService.selectedRangeChange.pipe(filter(() => !this._init), destructor).subscribe(() => { + this._activeRowIndexes = null; + }); } /** @@ -4055,9 +4064,14 @@ export abstract class IgxGridBaseDirective implements GridType, protected get activeRowIndexes(): number[] { - const activeRow = this.navigation.activeNode?.row; - const selectedCellIndexes = (this.selectionService.selection?.keys() as any)?.toArray(); - return [activeRow, ...selectedCellIndexes]; + if (this._activeRowIndexes) { + return this._activeRowIndexes; + } else { + const activeRow = this.navigation.activeNode?.row; + const selectedCellIndexes = (this.selectionService.selection?.keys() as any)?.toArray(); + this._activeRowIndexes = [activeRow, ...selectedCellIndexes]; + return this._activeRowIndexes; + } } protected get hasCellsToMerge() { diff --git a/projects/igniteui-angular/src/lib/grids/selection/selection.service.ts b/projects/igniteui-angular/src/lib/grids/selection/selection.service.ts index 83287824d16..0a92780d866 100644 --- a/projects/igniteui-angular/src/lib/grids/selection/selection.service.ts +++ b/projects/igniteui-angular/src/lib/grids/selection/selection.service.ts @@ -35,6 +35,11 @@ export class IgxGridSelectionService { */ public selectedRowsChange = new Subject(); + /** + * @hidden @internal + */ + public selectedRangeChange = new Subject>>(); + /** * Toggled when a pointerdown event is triggered inside the grid body (cells). * When `false` the drag select behavior is disabled. @@ -355,6 +360,8 @@ export class IgxGridSelectionService { } } } + + this.selectedRangeChange.next(collection); } public dragSelect(node: ISelectionNode, state: SelectionState): void { @@ -637,7 +644,7 @@ export class IgxGridSelectionService { if (this.areEqualCollections(currSelection, newSelection)) { return; } - + const args: IRowSelectionEventArgs = { owner: this.grid, oldSelection: currSelection, From 50e7d85a7dae9575aa97636f939da85580b4dc2f Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 18 Jul 2025 17:14:00 +0300 Subject: [PATCH 041/106] chore(*): When searching, mark merged cells as a single result. --- .../src/lib/grids/grid-base.directive.ts | 28 ++++++----- .../grid-cellMerging.component.html | 46 ++++++++++++++++++- .../grid-cellMerging.component.ts | 28 ++++++++++- 3 files changed, 89 insertions(+), 13 deletions(-) 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 43cd98538a7..c563367f506 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -38,7 +38,7 @@ import { IgcTrialWatermark } from 'igniteui-trial-watermark'; import { Subject, pipe, fromEvent, animationFrameScheduler, merge } from 'rxjs'; import { takeUntil, first, filter, throttleTime, map, shareReplay, takeWhile } from 'rxjs/operators'; import { cloneArray, mergeObjects, compareMaps, resolveNestedPath, isObject, PlatformUtil } from '../core/utils'; -import { GridColumnDataType } from '../data-operations/data-util'; +import { DataUtil, GridColumnDataType } from '../data-operations/data-util'; import { FilteringLogic } from '../data-operations/filtering-expression.interface'; import { IGroupByRecord } from '../data-operations/groupby-record.interface'; import { IForOfDataChangeEventArgs, IgxGridForOfDirective } from '../directives/for-of/for_of.directive'; @@ -3911,10 +3911,12 @@ export abstract class IgxGridBaseDirective implements GridType, this.activeNodeChange.pipe(filter(() => !this._init), destructor).subscribe(() => { this._activeRowIndexes = null; + this.refreshSearch(); }); this.selectionService.selectedRangeChange.pipe(filter(() => !this._init), destructor).subscribe(() => { this._activeRowIndexes = null; + this.refreshSearch(); }); } @@ -7977,25 +7979,29 @@ export abstract class IgxGridBaseDirective implements GridType, const caseSensitive = this._lastSearchInfo.caseSensitive; const exactMatch = this._lastSearchInfo.exactMatch; const searchText = caseSensitive ? this._lastSearchInfo.searchText : this._lastSearchInfo.searchText.toLowerCase(); - const data = this.filteredSortedData; + let data = this.filteredSortedData; + if (this.hasCellsToMerge) { + data = DataUtil.merge(cloneArray(this.filteredSortedData), this.columnsToMerge, this.mergeStrategy, this.activeRowIndexes, this); + } const columnItems = this.visibleColumns.filter((c) => !c.columnGroup).sort((c1, c2) => c1.visibleIndex - c2.visibleIndex); const columnsPathParts = columnItems.map(col => columnFieldPath(col.field)); data.forEach((dataRow, rowIndex) => { + const currentRowData = this.isRecordMerged(dataRow) ? dataRow.recordRef : dataRow; columnItems.forEach((c, cid) => { const pipeArgs = this.getColumnByName(c.field).pipeArgs; - const value = c.formatter ? c.formatter(resolveNestedPath(dataRow, columnsPathParts[cid]), dataRow) : - c.dataType === 'number' ? formatNumber(resolveNestedPath(dataRow, columnsPathParts[cid]) as number, this.locale, pipeArgs.digitsInfo) : + const value = c.formatter ? c.formatter(resolveNestedPath(currentRowData, columnsPathParts[cid]), currentRowData) : + c.dataType === 'number' ? formatNumber(resolveNestedPath(currentRowData, columnsPathParts[cid]) as number, this.locale, pipeArgs.digitsInfo) : c.dataType === 'date' - ? formatDate(resolveNestedPath(dataRow, columnsPathParts[cid]) as string, pipeArgs.format, this.locale, pipeArgs.timezone) - : resolveNestedPath(dataRow, columnsPathParts[cid]); + ? formatDate(resolveNestedPath(currentRowData, columnsPathParts[cid]) as string, pipeArgs.format, this.locale, pipeArgs.timezone) + : resolveNestedPath(currentRowData, columnsPathParts[cid]); if (value !== undefined && value !== null && c.searchable) { let searchValue = caseSensitive ? String(value) : String(value).toLowerCase(); - + const isMergePlaceHolder = this.isRecordMerged(dataRow) ? !!dataRow?.cellMergeMeta.get(c.field)?.root : false; if (exactMatch) { - if (searchValue === searchText) { + if (searchValue === searchText && !isMergePlaceHolder) { const mic: IMatchInfoCache = { - row: dataRow, + row: currentRowData, column: c.field, index: 0, metadata: new Map([['pinned', this.isRecordPinnedByIndex(rowIndex)]]) @@ -8007,9 +8013,9 @@ export abstract class IgxGridBaseDirective implements GridType, let occurrenceIndex = 0; let searchIndex = searchValue.indexOf(searchText); - while (searchIndex !== -1) { + while (searchIndex !== -1 && !isMergePlaceHolder) { const mic: IMatchInfoCache = { - row: dataRow, + row: currentRowData, column: c.field, index: occurrenceIndex++, metadata: new Map([['pinned', this.isRecordPinnedByIndex(rowIndex)]]) diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 15a44d78af3..b522b1af332 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -1,5 +1,49 @@

Grid with cell merge

- + + + @if (searchText.length === 0) { + search + } + @if (searchText.length > 0) { + clear + } + + + + + @if (searchText.length > 0) { + + @if (grid.lastSearchInfo) { +
+ @if (grid.lastSearchInfo.matchInfoCache.length > 0) { + + {{ grid.lastSearchInfo.activeMatchIndex + 1 }} of {{ grid.lastSearchInfo.matchInfoCache.length }} + results + + } + @if (grid.lastSearchInfo.matchInfoCache.length === 0) { + + No results + + } +
+ } +
+ + +
+
+ } +
+
+ diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index ec6eed2b1a3..89b86f37a5e 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -13,8 +13,13 @@ import { IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent, IgxHierarchicalGridComponent, + IgxIconComponent, + IgxInputDirective, + IgxInputGroupComponent, IgxPaginatorComponent, + IgxPrefixDirective, IgxRowIslandComponent, + IgxSuffixDirective, IgxTreeGridComponent } from 'igniteui-angular'; import { HIERARCHICAL_DATA } from '../shared/hierarchicalData'; @@ -43,7 +48,12 @@ import { ByLevelTreeGridMergeStrategy } from 'igniteui-angular'; IgxGridToolbarExporterComponent, IgxHierarchicalGridComponent, IgxRowIslandComponent, - IgxTreeGridComponent + IgxTreeGridComponent, + IgxInputGroupComponent, + IgxPrefixDirective, + IgxSuffixDirective, + IgxIconComponent, + IgxInputDirective ] }) export class GridCellMergingComponent { @@ -52,6 +62,8 @@ export class GridCellMergingComponent { public treeGridMergeStrategy = new ByLevelTreeGridMergeStrategy(); public alignBottom = { alignItems: "flex-end", paddingBottom: "12px"}; public alignTop= { alignItems: "flex-start", paddingTop: "12px" }; + public searchText: string =''; + @ViewChild('grid1', { static: true }) public grid: IgxGridComponent; public data = [{ ProductID: 1, ProductName: 'Chai', @@ -387,5 +399,19 @@ export class GridCellMergingComponent { OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() }]; + public searchKeyDown(ev) { + if (ev.key === 'Enter' || ev.key === 'ArrowDown' || ev.key === 'ArrowRight') { + ev.preventDefault(); + this.grid.findNext(this.searchText, true, false); + } else if (ev.key === 'ArrowUp' || ev.key === 'ArrowLeft') { + ev.preventDefault(); + this.grid.findPrev(this.searchText, true, false); + } + } + + public clearSearch() { + this.searchText = ''; + this.grid.clearSearch(); + } } From 1e560e8aca89e839fa8e87728949903fe488bc5c Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 18 Jul 2025 17:33:47 +0300 Subject: [PATCH 042/106] chore(*): Refresh search if needed only. --- .../igniteui-angular/src/lib/grids/grid-base.directive.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 c563367f506..8b3889b317c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3911,12 +3911,16 @@ export abstract class IgxGridBaseDirective implements GridType, this.activeNodeChange.pipe(filter(() => !this._init), destructor).subscribe(() => { this._activeRowIndexes = null; - this.refreshSearch(); + if (this.hasCellsToMerge) { + this.refreshSearch(); + } }); this.selectionService.selectedRangeChange.pipe(filter(() => !this._init), destructor).subscribe(() => { this._activeRowIndexes = null; - this.refreshSearch(); + if (this.hasCellsToMerge) { + this.refreshSearch(); + } }); } From 7db787aaa2477999633e98516bc164870f0507a1 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 21 Jul 2025 13:53:19 +0300 Subject: [PATCH 043/106] chore(*): Fix scrollTo when scrolling to a merged cell that has larger rowspan. --- .../src/lib/grids/grid-base.directive.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 8b3889b317c..e5bcaa27c3e 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -7633,15 +7633,21 @@ export abstract class IgxGridBaseDirective implements GridType, this.page = page; } } - + let targetRowIndex = (typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(row)); + const virtRec = this.verticalScrollContainer.igxForOf[targetRowIndex]; + const col = typeof (column) === 'number' ? this.visibleColumns[column] : column; + const rowSpan = this.isRecordMerged(virtRec) ? virtRec?.cellMergeMeta.get(col)?.rowSpan : 1; + if (rowSpan > 1) { + targetRowIndex += Math.floor(rowSpan/2); + } if (delayScrolling) { this.verticalScrollContainer.dataChanged.pipe(first(), takeUntil(this.destroy$)).subscribe(() => { this.scrollDirective(this.verticalScrollContainer, - typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(row)); + targetRowIndex); }); } else { this.scrollDirective(this.verticalScrollContainer, - typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(row)); + targetRowIndex); } this.scrollToHorizontally(column); From 86bc021d837cc0f1fe5ee20a0f1ef512ae5a73c0 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 21 Jul 2025 16:54:29 +0300 Subject: [PATCH 044/106] chore(*): Add basic merging tests. --- .../src/lib/grids/grid/cell-merge.spec.ts | 227 ++++++++++++++++++ .../src/lib/test-utils/grid-functions.spec.ts | 17 +- 2 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts new file mode 100644 index 00000000000..f5f62896f78 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -0,0 +1,227 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { DefaultMergeStrategy, GridCellMergeMode, GridColumnDataType, GridType, IgxColumnComponent, IgxGridComponent, SortingDirection } from 'igniteui-angular'; +import { DataParent } from '../../test-utils/sample-test-data.spec'; +import { GridFunctions } from '../../test-utils/grid-functions.spec'; + +describe('IgxGrid - Cell merging #grid', () => { + let fix; + let grid: IgxGridComponent; + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, DefaultCellMergeGridComponent + ] + }).compileComponents(); + })); + + beforeEach(() => { + fix = TestBed.createComponent(DefaultCellMergeGridComponent); + fix.detectChanges(); + grid = fix.componentInstance.grid; + }); + + describe('Basic', () => { + it('should allow enabling/disabling merging per column.', () => { + + const col = grid.getColumnByName('ProductName'); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 2 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null , span: 1 }, + { value: 'NetAdvantage' , span: 2 } + ]); + + // disable merge + col.merge = false; + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: null , span: 1 }, + { value: 'NetAdvantage' , span: 1 }, + { value: 'NetAdvantage' , span: 1 } + ]); + }); + + it('should always merge columns if mergeMode is always.', () => { + const col = grid.getColumnByName('Released'); + col.merge = true; + fix.detectChanges(); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: true, span: 9 } + ]); + }); + + it('should merge only sorted columns if mergeMode is onSort.', () => { + grid.cellMergeMode = 'onSort'; + fix.detectChanges(); + const col = grid.getColumnByName('ProductName'); + //nothing is merged initially + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: null , span: 1 }, + { value: 'NetAdvantage' , span: 1 }, + { value: 'NetAdvantage' , span: 1 } + ]); + + grid.sort({ fieldName: 'ProductName', dir: SortingDirection.Desc, ignoreCase: false }); + fix.detectChanges(); + + // merge only after sorted + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'NetAdvantage' , span: 2 }, + { value: 'Ignite UI for JavaScript', span: 3 }, + { value: 'Ignite UI for Angular', span: 3 }, + { value: null , span: 1 } + ]); + }); + + it('should allow setting a custom merge strategy via mergeStrategy on grid.', () => { + grid.mergeStrategy = new NoopMergeStrategy(); + fix.detectChanges(); + const col = grid.getColumnByName('ProductName'); + // this strategy does no merging + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: null , span: 1 }, + { value: 'NetAdvantage' , span: 1 }, + { value: 'NetAdvantage' , span: 1 } + ]); + }); + + it('should allow setting a custom comparer for merging on particular column via mergingComparer.', () => { + const col = grid.getColumnByName('ProductName'); + // all are same and should merge + col.mergingComparer = (prev:any, rec: any, field: string) => { + return true; + }; + grid.pipeTrigger += 1; + fix.detectChanges(); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 9 } + ]); + }); + }); +}); + +@Component({ + template: ` + + @for(col of cols; track col) { + + } + + `, + imports: [IgxGridComponent, IgxColumnComponent] +}) +export class DefaultCellMergeGridComponent extends DataParent { + public mergeMode: GridCellMergeMode = GridCellMergeMode.always; + @ViewChild('grid', { read: IgxGridComponent, static: true }) + public grid: IgxGridComponent; + public cols = [ + { field:'ID', merge: false }, + { field:'ProductName', dataType: GridColumnDataType.String, merge: true }, + { field:'Downloads', dataType: GridColumnDataType.Number, merge: false }, + { field:'Released', dataType: GridColumnDataType.Boolean, merge: false }, + { field:'ReleaseDate', dataType: GridColumnDataType.Date, merge: false } + ]; + + public override data = [ + { + Downloads: 254, + ID: 1, + ProductName: 'Ignite UI for JavaScript', + ReleaseDate: this.today, + Released: true + }, + { + Downloads: 1000, + ID: 2, + ProductName: 'Ignite UI for JavaScript', + ReleaseDate: this.nextDay, + Released: true + }, + { + Downloads: 20, + ID: 3, + ProductName: 'Ignite UI for Angular', + ReleaseDate: null, + Released: true + }, + { + Downloads: null, + ID: 4, + ProductName: 'Ignite UI for JavaScript', + ReleaseDate: this.prevDay, + Released: true + }, + { + Downloads: 100, + ID: 5, + ProductName: 'Ignite UI for Angular', + ReleaseDate: null, + Released: true + }, + { + Downloads: 1000, + ID: 6, + ProductName: 'Ignite UI for Angular', + ReleaseDate: this.nextDay, + Released: true + }, + { + Downloads: 0, + ID: 7, + ProductName: null, + ReleaseDate: this.prevDay, + Released: true + }, + { + Downloads: 1000, + ID: 8, + ProductName: 'NetAdvantage', + ReleaseDate: this.today, + Released: true + }, + { + Downloads: 1000, + ID: 9, + ProductName: 'NetAdvantage', + ReleaseDate: this.prevDay, + Released: true + } + ]; + +} + +class NoopMergeStrategy extends DefaultMergeStrategy { + public override merge( + data: any[], + field: string, + comparer: (prevRecord: any, record: any, field: string) => boolean = this.comparer, + result: any[], + activeRowIndexes : number[], + grid?: GridType + ) { + return data; + } +} diff --git a/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts index e6689a977de..53058f1328a 100644 --- a/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts @@ -19,7 +19,7 @@ import { IgxGridCellComponent } from '../grids/cell.component'; import { IgxPivotRowComponent } from '../grids/pivot-grid/pivot-row.component'; import { SortingDirection } from '../data-operations/sorting-strategy'; import { IgxRowDirective } from '../grids/row.directive'; -import { CellType, GridType, RowType } from '../grids/common/grid.interface'; +import { CellType, ColumnType, GridType, RowType } from '../grids/common/grid.interface'; import { IgxTreeNodeComponent } from '../tree/tree-node/tree-node.component'; import { IgxColumnComponent } from '../grids/columns/column.component'; import { IgxPivotGridComponent } from '../grids/pivot-grid/pivot-grid.component'; @@ -104,6 +104,21 @@ export const SAFE_DISPOSE_COMP_ID = 'root'; export class GridFunctions { + public static verifyColumnMergedState(grid: GridType, col: ColumnType, state: any[]) { + const dataRows = grid.dataRowList.toArray(); + let totalSpan = 0; + for (let index = 0; index < dataRows.length - 1; index++) { + const row = dataRows[index]; + const cellValue = row.cells.toArray().find(x => x.column === col).value; + const rowSpan = row.metaData?.cellMergeMeta.get(col.field)?.rowSpan || 1; + const currState = state[index - totalSpan]; + expect(cellValue).toBe(currState.value); + expect(rowSpan).toBe(currState.span); + totalSpan += (rowSpan - 1); + index += (rowSpan - 1); + } + } + public static getRows(fix): DebugElement[] { const rows: DebugElement[] = fix.debugElement.queryAll(By.css(ROW_CSS_CLASS)); rows.shift(); From 91c457b27f9e261cf15d8aa1cb69fc65cb51764a Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 21 Jul 2025 18:26:49 +0300 Subject: [PATCH 045/106] chore(*): Add some UI tests for merging. --- .../src/lib/grids/grid/cell-merge.spec.ts | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts index f5f62896f78..b52e0c929e6 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -1,13 +1,19 @@ -import { Component, ViewChild } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { DefaultMergeStrategy, GridCellMergeMode, GridColumnDataType, GridType, IgxColumnComponent, IgxGridComponent, SortingDirection } from 'igniteui-angular'; import { DataParent } from '../../test-utils/sample-test-data.spec'; import { GridFunctions } from '../../test-utils/grid-functions.spec'; +import { By } from '@angular/platform-browser'; +import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec'; +import { hasClass } from '../../test-utils/helper-utils.spec'; describe('IgxGrid - Cell merging #grid', () => { let fix; let grid: IgxGridComponent; + const MERGE_CELL_CSS_CLASS = '.igx-grid__td--merged'; + const CELL_CSS_CLASS = '.igx-grid__td'; + const CSS_CLASS_GRID_ROW = '.igx-grid__tr'; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -121,6 +127,38 @@ describe('IgxGrid - Cell merging #grid', () => { ]); }); }); + + + describe('UI', () => { + it ('should properly align merged cells with their spanned rows.', () => { + const mergedCell = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS))[0].nativeNode; + const endRow = fix.debugElement.queryAll(By.css(CSS_CLASS_GRID_ROW))[2].nativeNode; + expect(mergedCell.getBoundingClientRect().bottom).toBe(endRow.getBoundingClientRect().bottom); + }); + + it('should mark merged cell as hovered when hovering any row that intersects that cell.', () => { + const secondRow = fix.debugElement.queryAll(By.css(CSS_CLASS_GRID_ROW))[2]; + UIInteractions.hoverElement(secondRow.nativeNode); + fix.detectChanges(); + // hover 2nd row that intersects the merged cell in row 1 + const mergedCell = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS))[0].nativeNode; + // merged cell should be marked as hovered + hasClass(mergedCell, 'igx-grid__td--merged-hovered', true); + }); + + it('should set correct size to merged cell that spans multiple rows that have different sizes.', () => { + const col = grid.getColumnByName('ID'); + col.bodyTemplate = fix.componentInstance.customTemplate; + fix.detectChanges(); + grid.verticalScrollContainer.recalcUpdateSizes(); + grid.dataRowList.toArray().forEach(x => x.cdr.detectChanges()); + const mergedCell = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS))[0].nativeNode; + // one row is 100px, other is 200, 4px border + expect(mergedCell.getBoundingClientRect().height).toBe(100 + 200 + 4); + + + }); + }); }); @Component({ @@ -130,6 +168,9 @@ describe('IgxGrid - Cell merging #grid', () => { } + + + `, imports: [IgxGridComponent, IgxColumnComponent] }) @@ -137,6 +178,10 @@ export class DefaultCellMergeGridComponent extends DataParent { public mergeMode: GridCellMergeMode = GridCellMergeMode.always; @ViewChild('grid', { read: IgxGridComponent, static: true }) public grid: IgxGridComponent; + + @ViewChild('customTemplate', { read: TemplateRef, static: true }) + public customTemplate: TemplateRef; + public cols = [ { field:'ID', merge: false }, { field:'ProductName', dataType: GridColumnDataType.String, merge: true }, From a5c5566911789aa96ae481dea7fa2c3951d99529 Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 22 Jul 2025 17:40:07 +0300 Subject: [PATCH 046/106] chore(*): Add some integration tests. --- .../src/lib/grids/grid-base.directive.ts | 2 +- .../src/lib/grids/grid/cell-merge.spec.ts | 460 +++++++++++++----- 2 files changed, 341 insertions(+), 121 deletions(-) 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 e5bcaa27c3e..e47d720920b 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3891,7 +3891,7 @@ export abstract class IgxGridBaseDirective implements GridType, const rec = prevDataView[index]; if (rec.cellMergeMeta && // index + maxRowSpan is within view - startIndex <= (index + Math.max(...rec.cellMergeMeta.values().toArray().map(x => x.rowSpan)))) { + startIndex < (index + Math.max(...rec.cellMergeMeta.values().toArray().map(x => x.rowSpan)))) { data.push({record: rec, index: index }); } } diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts index b52e0c929e6..87710439b54 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -1,7 +1,7 @@ import { Component, TemplateRef, ViewChild } from '@angular/core'; import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { DefaultMergeStrategy, GridCellMergeMode, GridColumnDataType, GridType, IgxColumnComponent, IgxGridComponent, SortingDirection } from 'igniteui-angular'; +import { DefaultMergeStrategy, DefaultSortingStrategy, GridCellMergeMode, GridColumnDataType, GridType, IgxColumnComponent, IgxGridComponent, IgxPaginatorComponent, SortingDirection } from 'igniteui-angular'; import { DataParent } from '../../test-utils/sample-test-data.spec'; import { GridFunctions } from '../../test-utils/grid-functions.spec'; import { By } from '@angular/platform-browser'; @@ -22,142 +22,337 @@ describe('IgxGrid - Cell merging #grid', () => { }).compileComponents(); })); - beforeEach(() => { - fix = TestBed.createComponent(DefaultCellMergeGridComponent); - fix.detectChanges(); - grid = fix.componentInstance.grid; - }); + describe('Basic', () => { - it('should allow enabling/disabling merging per column.', () => { - - const col = grid.getColumnByName('ProductName'); - GridFunctions.verifyColumnMergedState(grid, col, [ - { value: 'Ignite UI for JavaScript', span: 2 }, - { value: 'Ignite UI for Angular', span: 1 }, - { value: 'Ignite UI for JavaScript', span: 1 }, - { value: 'Ignite UI for Angular', span: 2 }, - { value: null , span: 1 }, - { value: 'NetAdvantage' , span: 2 } - ]); - - // disable merge - col.merge = false; - fix.detectChanges(); - GridFunctions.verifyColumnMergedState(grid, col, [ - { value: 'Ignite UI for JavaScript', span: 1 }, - { value: 'Ignite UI for JavaScript', span: 1 }, - { value: 'Ignite UI for Angular', span: 1 }, - { value: 'Ignite UI for JavaScript', span: 1 }, - { value: 'Ignite UI for Angular', span: 1 }, - { value: 'Ignite UI for Angular', span: 1 }, - { value: null , span: 1 }, - { value: 'NetAdvantage' , span: 1 }, - { value: 'NetAdvantage' , span: 1 } - ]); + beforeEach(() => { + fix = TestBed.createComponent(DefaultCellMergeGridComponent); + fix.detectChanges(); + grid = fix.componentInstance.grid; }); - it('should always merge columns if mergeMode is always.', () => { - const col = grid.getColumnByName('Released'); - col.merge = true; - fix.detectChanges(); - GridFunctions.verifyColumnMergedState(grid, col, [ - { value: true, span: 9 } - ]); + describe('Configuration', () => { + + it('should allow enabling/disabling merging per column.', () => { + + const col = grid.getColumnByName('ProductName'); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 2 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + + // disable merge + col.merge = false; + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 1 }, + { value: 'NetAdvantage', span: 1 } + ]); + }); + + it('should always merge columns if mergeMode is always.', () => { + const col = grid.getColumnByName('Released'); + col.merge = true; + fix.detectChanges(); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: true, span: 9 } + ]); + }); + + it('should merge only sorted columns if mergeMode is onSort.', () => { + grid.cellMergeMode = 'onSort'; + fix.detectChanges(); + const col = grid.getColumnByName('ProductName'); + //nothing is merged initially + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 1 }, + { value: 'NetAdvantage', span: 1 } + ]); + + grid.sort({ fieldName: 'ProductName', dir: SortingDirection.Desc, ignoreCase: false }); + fix.detectChanges(); + + // merge only after sorted + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'NetAdvantage', span: 2 }, + { value: 'Ignite UI for JavaScript', span: 3 }, + { value: 'Ignite UI for Angular', span: 3 }, + { value: null, span: 1 } + ]); + }); + + it('should allow setting a custom merge strategy via mergeStrategy on grid.', () => { + grid.mergeStrategy = new NoopMergeStrategy(); + fix.detectChanges(); + const col = grid.getColumnByName('ProductName'); + // this strategy does no merging + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 1 }, + { value: 'NetAdvantage', span: 1 } + ]); + }); + + it('should allow setting a custom comparer for merging on particular column via mergingComparer.', () => { + const col = grid.getColumnByName('ProductName'); + // all are same and should merge + col.mergingComparer = (prev: any, rec: any, field: string) => { + return true; + }; + grid.pipeTrigger += 1; + fix.detectChanges(); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 9 } + ]); + }); }); - it('should merge only sorted columns if mergeMode is onSort.', () => { - grid.cellMergeMode = 'onSort'; - fix.detectChanges(); - const col = grid.getColumnByName('ProductName'); - //nothing is merged initially - GridFunctions.verifyColumnMergedState(grid, col, [ - { value: 'Ignite UI for JavaScript', span: 1 }, - { value: 'Ignite UI for JavaScript', span: 1 }, - { value: 'Ignite UI for Angular', span: 1 }, - { value: 'Ignite UI for JavaScript', span: 1 }, - { value: 'Ignite UI for Angular', span: 1 }, - { value: 'Ignite UI for Angular', span: 1 }, - { value: null , span: 1 }, - { value: 'NetAdvantage' , span: 1 }, - { value: 'NetAdvantage' , span: 1 } - ]); - - grid.sort({ fieldName: 'ProductName', dir: SortingDirection.Desc, ignoreCase: false }); - fix.detectChanges(); + describe('UI', () => { + it('should properly align merged cells with their spanned rows.', () => { + const mergedCell = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS))[0].nativeNode; + const endRow = fix.debugElement.queryAll(By.css(CSS_CLASS_GRID_ROW))[2].nativeNode; + expect(mergedCell.getBoundingClientRect().bottom).toBe(endRow.getBoundingClientRect().bottom); + }); + + it('should mark merged cell as hovered when hovering any row that intersects that cell.', () => { + const secondRow = fix.debugElement.queryAll(By.css(CSS_CLASS_GRID_ROW))[2]; + UIInteractions.hoverElement(secondRow.nativeNode); + fix.detectChanges(); + // hover 2nd row that intersects the merged cell in row 1 + const mergedCell = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS))[0].nativeNode; + // merged cell should be marked as hovered + hasClass(mergedCell, 'igx-grid__td--merged-hovered', true); + }); - // merge only after sorted - GridFunctions.verifyColumnMergedState(grid, col, [ - { value: 'NetAdvantage' , span: 2 }, - { value: 'Ignite UI for JavaScript', span: 3 }, - { value: 'Ignite UI for Angular', span: 3 }, - { value: null , span: 1 } - ]); + it('should set correct size to merged cell that spans multiple rows that have different sizes.', () => { + const col = grid.getColumnByName('ID'); + col.bodyTemplate = fix.componentInstance.customTemplate; + fix.detectChanges(); + grid.verticalScrollContainer.recalcUpdateSizes(); + grid.dataRowList.toArray().forEach(x => x.cdr.detectChanges()); + const mergedCell = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS))[0].nativeNode; + // one row is 100px, other is 200, 4px border + expect(mergedCell.getBoundingClientRect().height).toBe(100 + 200 + 4); + }); }); + }); - it('should allow setting a custom merge strategy via mergeStrategy on grid.', () => { - grid.mergeStrategy = new NoopMergeStrategy(); + describe('Integration', () => { + beforeEach(() => { + fix = TestBed.createComponent(IntegrationCellMergeGridComponent); fix.detectChanges(); - const col = grid.getColumnByName('ProductName'); - // this strategy does no merging - GridFunctions.verifyColumnMergedState(grid, col, [ - { value: 'Ignite UI for JavaScript', span: 1 }, - { value: 'Ignite UI for JavaScript', span: 1 }, - { value: 'Ignite UI for Angular', span: 1 }, - { value: 'Ignite UI for JavaScript', span: 1 }, - { value: 'Ignite UI for Angular', span: 1 }, - { value: 'Ignite UI for Angular', span: 1 }, - { value: null , span: 1 }, - { value: 'NetAdvantage' , span: 1 }, - { value: 'NetAdvantage' , span: 1 } - ]); + grid = fix.componentInstance.grid; }); - it('should allow setting a custom comparer for merging on particular column via mergingComparer.', () => { - const col = grid.getColumnByName('ProductName'); - // all are same and should merge - col.mergingComparer = (prev:any, rec: any, field: string) => { - return true; - }; - grid.pipeTrigger += 1; - fix.detectChanges(); - GridFunctions.verifyColumnMergedState(grid, col, [ - { value: 'Ignite UI for JavaScript', span: 9 } - ]); + describe('Virtualization', () => { + beforeEach(() => { + fix.componentInstance.width = '400px'; + fix.componentInstance.height = '300px'; + fix.detectChanges(); + }); + it('should retain rows with merged cells that span multiple rows in DOM as long as merged cell is still in view.', async() => { + // initial row list is same as the virtualization chunk + expect(grid.rowList.length).toBe(grid.virtualizationState.chunkSize); + + grid.navigateTo(grid.virtualizationState.chunkSize - 1, 0); + await wait(100); + fix.detectChanges(); + + //virtualization starts from 1 + expect(grid.virtualizationState.startIndex).toBe(1); + + // check row is chunkSize + 1 extra row at the top + expect(grid.rowList.length).toBe(grid.virtualizationState.chunkSize + 1); + // first row at top is index 0 + expect(grid.rowList.first.index).toBe(0); + // and has offset to position correctly the merged cell + expect(grid.rowList.first.nativeElement.offsetTop).toBeLessThan(-50); + }); + + it('should remove row from DOM when merged cell is no longer in view.', async() => { + // scroll so that first row with merged cell is not in view + grid.navigateTo(grid.virtualizationState.chunkSize, 0); + await wait(100); + fix.detectChanges(); + + //virtualization starts from 2 + expect(grid.virtualizationState.startIndex).toBe(2); + + // no merge cells from previous chunks + expect(grid.rowList.length).toBe(grid.virtualizationState.chunkSize); + // first row is from the virtualization + expect(grid.rowList.first.index).toBe(grid.virtualizationState.startIndex); + }); + + it('horizontal virtualization should not be affected by vertically merged cells.', async() => { + let mergedCell = grid.rowList.first.cells.find(x => x.column.field === 'ProductName'); + expect(mergedCell.value).toBe('Ignite UI for JavaScript'); + expect(mergedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("51px 51px"); + + // scroll horizontally + grid.navigateTo(0, 4); + await wait(100); + fix.detectChanges(); + + // not in DOM + mergedCell = grid.rowList.first.cells.find(x => x.column.field === 'ProductName'); + expect(mergedCell).toBeUndefined(); + + // scroll back + grid.navigateTo(0, 0); + await wait(100); + fix.detectChanges(); + + mergedCell = grid.rowList.first.cells.find(x => x.column.field === 'ProductName'); + expect(mergedCell.value).toBe('Ignite UI for JavaScript'); + expect(mergedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("51px 51px"); + }); }); - }); + describe('Group By', () => { + it('cells should merge only within their respective groups.', () => { + grid.groupBy({ + fieldName: 'ProductName', dir: SortingDirection.Desc, + ignoreCase: false, strategy: DefaultSortingStrategy.instance() + }); + fix.detectChanges(); + + const col = grid.getColumnByName('ProductName'); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'NetAdvantage', span: 2 }, + { value: 'Ignite UI for JavaScript', span: 3 }, + { value: 'Ignite UI for Angular', span: 3 }, + { value: null, span: 1 } + ]); + + grid.groupBy({ + fieldName: 'ReleaseDate', dir: SortingDirection.Desc, + ignoreCase: false, strategy: DefaultSortingStrategy.instance() + }); + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'NetAdvantage', span: 1 }, + { value: 'NetAdvantage', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 } + ]); + + }); - describe('UI', () => { - it ('should properly align merged cells with their spanned rows.', () => { - const mergedCell = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS))[0].nativeNode; - const endRow = fix.debugElement.queryAll(By.css(CSS_CLASS_GRID_ROW))[2].nativeNode; - expect(mergedCell.getBoundingClientRect().bottom).toBe(endRow.getBoundingClientRect().bottom); }); - it('should mark merged cell as hovered when hovering any row that intersects that cell.', () => { - const secondRow = fix.debugElement.queryAll(By.css(CSS_CLASS_GRID_ROW))[2]; - UIInteractions.hoverElement(secondRow.nativeNode); - fix.detectChanges(); - // hover 2nd row that intersects the merged cell in row 1 - const mergedCell = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS))[0].nativeNode; - // merged cell should be marked as hovered - hasClass(mergedCell, 'igx-grid__td--merged-hovered', true); + describe('Master-Detail', () => { + + it('should interrupt merge sequence if a master-detail row is expanded.', () => { + grid.detailTemplate = fix.componentInstance.detailTemplate; + fix.detectChanges(); + + const col = grid.getColumnByName('ProductName'); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 2 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + + GridFunctions.toggleMasterRow(fix, grid.rowList.first); + fix.detectChanges(); + + // should slit first merge group in 2 + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + }); + + }); - it('should set correct size to merged cell that spans multiple rows that have different sizes.', () => { - const col = grid.getColumnByName('ID'); - col.bodyTemplate = fix.componentInstance.customTemplate; - fix.detectChanges(); - grid.verticalScrollContainer.recalcUpdateSizes(); - grid.dataRowList.toArray().forEach(x => x.cdr.detectChanges()); - const mergedCell = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS))[0].nativeNode; - // one row is 100px, other is 200, 4px border - expect(mergedCell.getBoundingClientRect().height).toBe(100 + 200 + 4); + describe('Paging', () => { + it('should merge cells only on current page of data.', () => { + fix.componentInstance.paging = true; + fix.detectChanges(); + grid.triggerPipes(); + fix.detectChanges(); + + const col = grid.getColumnByName('ProductName'); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 2 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 } + ]); + grid.page = 2; + fix.detectChanges(); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for Angular', span: 1 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + }); }); + + describe('Column Pinning', () => { + it('should merge cells in pinned columns.', () => { + const col = grid.getColumnByName('ProductName'); + col.pinned = true; + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 2 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + + const mergedCell = grid.rowList.first.cells.find(x => x.column.field === 'ProductName'); + expect(mergedCell.value).toBe('Ignite UI for JavaScript'); + expect(mergedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("51px 51px"); + }); + }); + }); }); @@ -183,11 +378,11 @@ export class DefaultCellMergeGridComponent extends DataParent { public customTemplate: TemplateRef; public cols = [ - { field:'ID', merge: false }, - { field:'ProductName', dataType: GridColumnDataType.String, merge: true }, - { field:'Downloads', dataType: GridColumnDataType.Number, merge: false }, - { field:'Released', dataType: GridColumnDataType.Boolean, merge: false }, - { field:'ReleaseDate', dataType: GridColumnDataType.Date, merge: false } + { field: 'ID', merge: false }, + { field: 'ProductName', dataType: GridColumnDataType.String, merge: true }, + { field: 'Downloads', dataType: GridColumnDataType.Number, merge: false }, + { field: 'Released', dataType: GridColumnDataType.Boolean, merge: false }, + { field: 'ReleaseDate', dataType: GridColumnDataType.Date, merge: false } ]; public override data = [ @@ -258,13 +453,38 @@ export class DefaultCellMergeGridComponent extends DataParent { } +@Component({ + template: ` + + @for(col of cols; track col) { + + } + @if (paging) { + + } + + + + + `, + imports: [IgxGridComponent, IgxColumnComponent, IgxPaginatorComponent] +}) +export class IntegrationCellMergeGridComponent extends DefaultCellMergeGridComponent { + public height = '100%'; + public width = '100%'; + public paging = false; + + @ViewChild('detailTemplate', { read: TemplateRef, static: true }) + public detailTemplate: TemplateRef; +} + class NoopMergeStrategy extends DefaultMergeStrategy { public override merge( data: any[], field: string, comparer: (prevRecord: any, record: any, field: string) => boolean = this.comparer, result: any[], - activeRowIndexes : number[], + activeRowIndexes: number[], grid?: GridType ) { return data; From 46ea7ae3cf27f111415fccde2151fa52556cbdae Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 23 Jul 2025 14:09:14 +0300 Subject: [PATCH 047/106] chore(*): Add more integration tests. --- .../src/lib/grids/grid/cell-merge.spec.ts | 186 +++++++++++++++++- .../src/lib/grids/row.directive.ts | 2 +- 2 files changed, 186 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts index 87710439b54..0229ef9af01 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -3,7 +3,7 @@ import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { DefaultMergeStrategy, DefaultSortingStrategy, GridCellMergeMode, GridColumnDataType, GridType, IgxColumnComponent, IgxGridComponent, IgxPaginatorComponent, SortingDirection } from 'igniteui-angular'; import { DataParent } from '../../test-utils/sample-test-data.spec'; -import { GridFunctions } from '../../test-utils/grid-functions.spec'; +import { GridFunctions, GridSelectionFunctions } from '../../test-utils/grid-functions.spec'; import { By } from '@angular/platform-browser'; import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec'; import { hasClass } from '../../test-utils/helper-utils.spec'; @@ -353,6 +353,190 @@ describe('IgxGrid - Cell merging #grid', () => { }); }); + describe('Row Pinning', () => { + it('should merge adjacent pinned rows in pinned row area.', () => { + const row1 = grid.rowList.toArray()[0]; + const row2 = grid.rowList.toArray()[1]; + const col = grid.getColumnByName('ProductName'); + row1.pin(); + row2.pin(); + fix.detectChanges(); + + expect(grid.pinnedRows.length).toBe(2); + const pinnedRow = grid.pinnedRows[0]; + expect(pinnedRow.metaData.cellMergeMeta.get(col.field)?.rowSpan).toBe(2); + const mergedPinnedCell = pinnedRow.cells.find(x => x.column.field === 'ProductName'); + expect(mergedPinnedCell.value).toBe('Ignite UI for JavaScript'); + expect(mergedPinnedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("51px 51px"); + }); + + it('should merge adjacent ghost rows in unpinned area.', () => { + const row1 = grid.rowList.toArray()[0]; + const row2 = grid.rowList.toArray()[1]; + const col = grid.getColumnByName('ProductName'); + row1.pin(); + row2.pin(); + fix.detectChanges(); + + const ghostRows = grid.rowList.filter(x => x.disabled); + expect(ghostRows.length).toBe(2); + const ghostRow = ghostRows[0]; + expect(ghostRow.metaData.cellMergeMeta.get(col.field)?.rowSpan).toBe(2); + const mergedPinnedCell = ghostRow.cells.find(x => x.column.field === 'ProductName'); + expect(mergedPinnedCell.value).toBe('Ignite UI for JavaScript'); + expect(mergedPinnedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("51px 51px"); + }); + + it('should not merge ghost and data rows together.', () => { + const col = grid.getColumnByName('ProductName'); + const row1 = grid.rowList.toArray()[0]; + row1.pin(); + fix.detectChanges(); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + }); + }); + + describe('Activation', () => { + + it('should interrupt merge sequence so that active row has no merging.', () => { + const col = grid.getColumnByName('ProductName'); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 2 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + + const row1 = grid.rowList.toArray()[0]; + + UIInteractions.simulateClickAndSelectEvent(row1.cells.toArray()[1].nativeElement); + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + }); + + }); + + describe('Updating', () => { + + beforeEach(() => { + grid.primaryKey = 'ID'; + grid.columns.forEach(x => x.editable = true); + fix.detectChanges(); + }); + + it('should edit the individual row values for the active row.', () => { + const col = grid.getColumnByName('ProductName'); + grid.rowEditable = true; + fix.detectChanges(); + + const row = grid.gridAPI.get_row_by_index(0); + const cell = grid.gridAPI.get_cell_by_index(0, 'ProductName'); + UIInteractions.simulateDoubleClickAndSelectEvent(cell.nativeElement); + fix.detectChanges(); + expect(row.inEditMode).toBe(true); + + // row in edit is not merged anymore + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + + // enter new val + const cellInput = grid.gridAPI.get_cell_by_index(0, 'ProductName').nativeElement.querySelector('[igxinput]'); + UIInteractions.setInputElementValue(cellInput, "NewValue"); + fix.detectChanges(); + + // Done button click + const doneButtonElement = GridFunctions.getRowEditingDoneButton(fix); + doneButtonElement.click(); + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'NewValue', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + }); + + it('should edit the individual cell value for the active row.', () => { + const col = grid.getColumnByName('ProductName'); + let cell = grid.gridAPI.get_cell_by_index(0, 'ProductName'); + + UIInteractions.simulateDoubleClickAndSelectEvent(cell.nativeElement); + fix.detectChanges(); + + cell = grid.gridAPI.get_cell_by_index(0, 'ProductName'); + expect(cell.editMode).toBe(true); + + // enter new val + const cellInput = grid.gridAPI.get_cell_by_index(0, 'ProductName').nativeElement.querySelector('[igxinput]'); + UIInteractions.setInputElementValue(cellInput, "NewValue"); + fix.detectChanges(); + + UIInteractions.triggerEventHandlerKeyDown('enter', GridFunctions.getGridContent(fix)); + fix.detectChanges(); + + // row with edit cell is not merged anymore + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'NewValue', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + }); + }); + describe('Row Selection', () => { + + it('should mark all merged cells that intersect with a selected row as selected.', () => { + grid.rowSelection = 'multiple'; + fix.detectChanges(); + + const secondRow = grid.rowList.toArray()[1]; + GridSelectionFunctions.clickRowCheckbox(secondRow); + fix.detectChanges(); + + expect(secondRow.selected).toBe(true); + grid.markForCheck(); + + const mergedIntersectedCell = grid.gridAPI.get_cell_by_index(0, 'ProductName'); + // check cell has selected style + hasClass(mergedIntersectedCell.nativeElement,'igx-grid__td--merged-selected', true); + }); + + }); + }); }); diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index 0e6c12ef6b7..a7bf2a10cb7 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -628,7 +628,7 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { if (mergeMeta && rowCount > 1) { const indexInData = this.pinned && this.grid.isRowPinningToTop ? this.index - this.grid.pinnedRecordsCount : this.index; const range = this.grid.verticalScrollContainer.igxForOf.slice(indexInData, indexInData + rowCount); - const inRange = range.filter(x => this.selectionService.isRowSelected(this.grid.primaryKey ? (x.recordRef || x)[this.grid.primaryKey] : (x.recordRef || x).recordRef)).length > 0; + const inRange = range.filter(x => this.selectionService.isRowSelected(this.grid.primaryKey ? (x.recordRef || x)[this.grid.primaryKey] : (x.recordRef || x))).length > 0; return inRange; } return false; From f8624690fd01254b48b0ca8a33729e0f856b283f Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 23 Jul 2025 16:02:36 +0300 Subject: [PATCH 048/106] chore(*): Add more integration tests. --- .../src/lib/grids/grid/cell-merge.spec.ts | 146 +++++++++++++++++- 1 file changed, 143 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts index 0229ef9af01..5b0df570546 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -1,12 +1,13 @@ import { Component, TemplateRef, ViewChild } from '@angular/core'; import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { DefaultMergeStrategy, DefaultSortingStrategy, GridCellMergeMode, GridColumnDataType, GridType, IgxColumnComponent, IgxGridComponent, IgxPaginatorComponent, SortingDirection } from 'igniteui-angular'; +import { DefaultMergeStrategy, DefaultSortingStrategy, GridCellMergeMode, GridColumnDataType, GridType, IgxColumnComponent, IgxGridComponent, IgxPaginatorComponent, IgxStringFilteringOperand, SortingDirection } from 'igniteui-angular'; import { DataParent } from '../../test-utils/sample-test-data.spec'; import { GridFunctions, GridSelectionFunctions } from '../../test-utils/grid-functions.spec'; import { By } from '@angular/platform-browser'; import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec'; import { hasClass } from '../../test-utils/helper-utils.spec'; +import { ColumnLayoutTestComponent } from './grid.multi-row-layout.spec'; describe('IgxGrid - Cell merging #grid', () => { let fix; @@ -14,10 +15,12 @@ describe('IgxGrid - Cell merging #grid', () => { const MERGE_CELL_CSS_CLASS = '.igx-grid__td--merged'; const CELL_CSS_CLASS = '.igx-grid__td'; const CSS_CLASS_GRID_ROW = '.igx-grid__tr'; + const HIGHLIGHT_ACTIVE_CSS_CLASS = '.igx-highlight__active'; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - NoopAnimationsModule, DefaultCellMergeGridComponent + NoopAnimationsModule, DefaultCellMergeGridComponent, ColumnLayoutTestComponent ] }).compileComponents(); })); @@ -517,6 +520,7 @@ describe('IgxGrid - Cell merging #grid', () => { ]); }); }); + describe('Row Selection', () => { it('should mark all merged cells that intersect with a selected row as selected.', () => { @@ -528,7 +532,6 @@ describe('IgxGrid - Cell merging #grid', () => { fix.detectChanges(); expect(secondRow.selected).toBe(true); - grid.markForCheck(); const mergedIntersectedCell = grid.gridAPI.get_cell_by_index(0, 'ProductName'); // check cell has selected style @@ -537,6 +540,143 @@ describe('IgxGrid - Cell merging #grid', () => { }); + describe('Cell Selection', () => { + it('should interrupt merge sequence so that selected cell has no merging.', () => { + const col = grid.getColumnByName('ProductName'); + grid.cellSelection = 'multiple'; + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 2 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 2 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + + const startCell = grid.gridAPI.get_cell_by_index(4, 'ProductName'); + const endCell = grid.gridAPI.get_cell_by_index(0, 'ID'); + + GridSelectionFunctions.selectCellsRangeNoWait(fix, startCell, endCell); + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for JavaScript', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: 'Ignite UI for Angular', span: 1 }, + { value: null, span: 1 }, + { value: 'NetAdvantage', span: 2 } + ]); + + // check api + expect(grid.getSelectedData().length).toBe(5); + expect(grid.getSelectedData()).toEqual(grid.data.slice(0, 5).map(x => { return { 'ID': x.ID, 'ProductName': x. ProductName}})); + }); + }); + + describe('Column selection', () => { + it('should mark merged cells in selected column as selected.', () => { + grid.columnSelection = 'multiple'; + fix.detectChanges(); + const col = grid.getColumnByName('ProductName'); + col.selected = true; + fix.detectChanges(); + + const mergedCells = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS)); + mergedCells.forEach(element => { + hasClass(element.nativeNode, 'igx-grid__td--column-selected', true); + }); + }); + + it('selected data API should return all associated data fields as selected.', () => { + grid.columnSelection = 'multiple'; + fix.detectChanges(); + const col = grid.getColumnByName('ProductName'); + col.selected = true; + fix.detectChanges(); + + expect(grid.getSelectedColumnsData()).toEqual(grid.data.map(x => { return {'ProductName': x. ProductName}})); + }); + }); + + describe('Filtering', () => { + + it('should merge cells in filtered data.', () => { + grid.filter('ProductName', 'Net', IgxStringFilteringOperand.instance().condition('startsWith'), true); + fix.detectChanges(); + const col = grid.getColumnByName('ProductName'); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'NetAdvantage', span: 2 } + ]); + }); + + }); + + describe('Searching', () => { + + it('findNext \ findPrev should count merged cells as 1 result and navigate once through them.', () => { + const cell0 = grid.gridAPI.get_cell_by_index(0, 'ProductName').nativeElement; + const cell3 = grid.gridAPI.get_cell_by_index(3, 'ProductName').nativeElement; + const fixNativeElem = fix.debugElement.nativeElement; + + let matches = grid.findNext('JavaScript'); + fix.detectChanges(); + + expect(matches).toBe(2); + + let activeHighlight = fixNativeElem.querySelectorAll(HIGHLIGHT_ACTIVE_CSS_CLASS); + expect(activeHighlight[0].closest("igx-grid-cell")).toBe(cell0); + + matches = grid.findNext('JavaScript'); + fix.detectChanges(); + + activeHighlight = fixNativeElem.querySelectorAll(HIGHLIGHT_ACTIVE_CSS_CLASS); + expect(activeHighlight[0].closest("igx-grid-cell")).toBe(cell3); + + matches = grid.findPrev('JavaScript'); + fix.detectChanges(); + + activeHighlight = fixNativeElem.querySelectorAll(HIGHLIGHT_ACTIVE_CSS_CLASS); + expect(activeHighlight[0].closest("igx-grid-cell")).toBe(cell0); + }); + + it('should update matches if a cell becomes unmerged.', () => { + let matches = grid.findNext('JavaScript'); + fix.detectChanges(); + + expect(matches).toBe(2); + + UIInteractions.simulateClickAndSelectEvent(grid.gridAPI.get_cell_by_index(0, 'ProductName').nativeElement); + fix.detectChanges(); + + matches = grid.findNext('JavaScript'); + fix.detectChanges(); + expect(matches).toBe(3); + }); + + }); + + describe('Multi-row layout', () => { + it('should throw warning and disallow merging with mrl.', () => { + jasmine.getEnv().allowRespy(true); + fix = TestBed.createComponent(ColumnLayoutTestComponent); + fix.detectChanges(); + grid = fix.componentInstance.grid; + spyOn(console, 'warn'); + grid.columns[1].merge = true; + fix.detectChanges(); + + expect(console.warn).toHaveBeenCalledWith('Merging is not supported with multi-row layouts.'); + expect(console.warn).toHaveBeenCalledTimes(1); + jasmine.getEnv().allowRespy(false); + }); + + }); + }); }); From fc467396f89401c67edb5578b700d4ff47862737 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 24 Jul 2025 14:40:33 +0300 Subject: [PATCH 049/106] chore(*): Add Hgrid and TreeGrid integration tests. --- .../src/lib/grids/grid/cell-merge.spec.ts | 209 +++++++++++++++++- 1 file changed, 207 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts index 5b0df570546..8e8bb8e58fb 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -1,13 +1,16 @@ import { Component, TemplateRef, ViewChild } from '@angular/core'; import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { DefaultMergeStrategy, DefaultSortingStrategy, GridCellMergeMode, GridColumnDataType, GridType, IgxColumnComponent, IgxGridComponent, IgxPaginatorComponent, IgxStringFilteringOperand, SortingDirection } from 'igniteui-angular'; +import { ByLevelTreeGridMergeStrategy, DefaultMergeStrategy, DefaultSortingStrategy, GridCellMergeMode, GridColumnDataType, GridType, IgxColumnComponent, IgxGridComponent, IgxHierarchicalGridComponent, IgxPaginatorComponent, IgxStringFilteringOperand, SortingDirection } from 'igniteui-angular'; import { DataParent } from '../../test-utils/sample-test-data.spec'; import { GridFunctions, GridSelectionFunctions } from '../../test-utils/grid-functions.spec'; import { By } from '@angular/platform-browser'; import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec'; import { hasClass } from '../../test-utils/helper-utils.spec'; import { ColumnLayoutTestComponent } from './grid.multi-row-layout.spec'; +import { IgxHierarchicalGridTestBaseComponent } from '../hierarchical-grid/hierarchical-grid.spec'; +import { IgxHierarchicalRowComponent } from '../hierarchical-grid/hierarchical-row.component'; +import { IgxTreeGridSelectionComponent } from '../../test-utils/tree-grid-components.spec'; describe('IgxGrid - Cell merging #grid', () => { let fix; @@ -20,7 +23,8 @@ describe('IgxGrid - Cell merging #grid', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - NoopAnimationsModule, DefaultCellMergeGridComponent, ColumnLayoutTestComponent + NoopAnimationsModule, DefaultCellMergeGridComponent, ColumnLayoutTestComponent, + IgxHierarchicalGridTestBaseComponent, IgxTreeGridSelectionComponent ] }).compileComponents(); })); @@ -677,6 +681,207 @@ describe('IgxGrid - Cell merging #grid', () => { }); + describe('HierarchicalGrid', () => { + + beforeEach(() => { + fix = TestBed.createComponent(IgxHierarchicalGridTestBaseComponent); + fix.componentInstance.data = [ + { + ID: 1, ChildLevels: 1, ProductName: 'Product A' , Col1: 1, + childData: [ + { + ID: 1, ChildLevels: 2, ProductName: 'Product A' , Col1: 1, + }, + { + ID: 2, ChildLevels: 2, ProductName: 'Product A' , Col1: 1, + }, + { + ID: 3, ChildLevels: 2, ProductName: 'Product B' , Col1: 1, + }, + { + ID: 4, ChildLevels: 2, ProductName: 'Product A' , Col1: 1, + } + ] + }, + { + ID: 2, ChildLevels: 1, ProductName: 'Product A' , Col1: 1, childData: [ + { + ID: 1, ChildLevels: 2, ProductName: 'Product A' , Col1: 1, + }, + { + ID: 2, ChildLevels: 2, ProductName: 'Product A' , Col1: 1, + }, + { + ID: 3, ChildLevels: 2, ProductName: 'Product A' , Col1: 1, + }, + { + ID: 4, ChildLevels: 2, ProductName: 'Product A' , Col1: 1, + } + ] + }, + { + ID: 3, ChildLevels: 1, ProductName: 'Product B' , Col1: 1 + }, + { + ID: 4, ChildLevels: 1, ProductName: 'Product B' , Col1: 1 + }, + { + ID: 5, ChildLevels: 1, ProductName: 'Product C' , Col1: 1 + }, + { + ID: 6, ChildLevels: 1, ProductName: 'Product B' , Col1: 1 + } + ]; + fix.detectChanges(); + grid = fix.componentInstance.hgrid; + // enable merging + grid.cellMergeMode = 'always'; + const col = grid.getColumnByName('ProductName'); + col.merge = true; + fix.detectChanges(); + }); + + it('should allow configuring and merging cells on each level of hierarchy.', () => { + + const col = grid.getColumnByName('ProductName'); + // root grid should be merged + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Product A', span: 2 }, + { value: 'Product B', span: 2 }, + { value: 'Product C', span: 1 }, + { value: 'Product B', span: 1 } + ]); + + const ri = fix.componentInstance.rowIsland; + ri.cellMergeMode = 'always'; + ri.getColumnByName('ProductName').merge = true; + fix.detectChanges(); + + // toggle row + const firstRow = grid.gridAPI.get_row_by_index(0) as IgxHierarchicalRowComponent; + firstRow.toggle(); + fix.detectChanges(); + + const childGrid = grid.gridAPI.getChildGrids(false)[0] as IgxHierarchicalGridComponent; + expect(childGrid).toBeDefined(); + + // merging enabled + GridFunctions.verifyColumnMergedState(childGrid, childGrid.getColumnByName('ProductName'), [ + { value: 'Product A', span: 2 }, + { value: 'Product B', span: 1 }, + { value: 'Product A', span: 1 } + ]); + }); + + it('should merge cells within their respective grids only.', () => { + const ri = fix.componentInstance.rowIsland; + ri.cellMergeMode = 'always'; + ri.getColumnByName('ProductName').merge = true; + fix.detectChanges(); + + // toggle row 1 + const firstRow = grid.gridAPI.get_row_by_index(0) as IgxHierarchicalRowComponent; + firstRow.toggle(); + fix.detectChanges(); + + // toggle row 2 + const secondRow = grid.gridAPI.get_row_by_index(2) as IgxHierarchicalRowComponent; + secondRow.toggle(); + fix.detectChanges(); + + const childGrid1 = grid.gridAPI.getChildGrids(false)[0] as IgxHierarchicalGridComponent; + expect(childGrid1).toBeDefined(); + + GridFunctions.verifyColumnMergedState(childGrid1, childGrid1.getColumnByName('ProductName'), [ + { value: 'Product A', span: 2 }, + { value: 'Product B', span: 1 }, + { value: 'Product A', span: 1 } + ]); + + const childGrid2 = grid.gridAPI.getChildGrids(false)[1] as IgxHierarchicalGridComponent; + expect(childGrid2).toBeDefined(); + + GridFunctions.verifyColumnMergedState(childGrid2, childGrid2.getColumnByName('ProductName'), [ + { value: 'Product A', span: 4 } + ]); + }); + + it('should interrupt merge sequence if row is expanded and a child grid is shown between same value cells.', () => { + const col = grid.getColumnByName('ProductName'); + // root grid should be merged + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Product A', span: 2 }, + { value: 'Product B', span: 2 }, + { value: 'Product C', span: 1 }, + { value: 'Product B', span: 1 } + ]); + + // toggle row 1 + const firstRow = grid.gridAPI.get_row_by_index(0) as IgxHierarchicalRowComponent; + firstRow.toggle(); + fix.detectChanges(); + + // first merge sequence interrupted due to expanded row + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: 'Product A', span: 1 }, + { value: 'Product A', span: 1 }, + { value: 'Product B', span: 2 }, + { value: 'Product C', span: 1 }, + { value: 'Product B', span: 1 } + ]); + }); + + }); + + describe('TreeGrid', () => { + + beforeEach(() => { + fix = TestBed.createComponent(IgxTreeGridSelectionComponent); + fix.detectChanges(); + grid = fix.componentInstance.treeGrid; + // enable merging + grid.cellMergeMode = 'always'; + const col = grid.getColumnByName('OnPTO'); + col.merge = true; + fix.detectChanges(); + }); + + it('should merge all cells with same values, even if on different levels by default.', () => { + const col = grid.getColumnByName('OnPTO'); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: false, span: 2 }, + { value: true, span: 1 }, + { value: false, span: 1 }, + { value: true, span: 1 }, + { value: false, span: 2 }, + { value: true, span: 1 }, + { value: false, span: 3 }, + { value: true, span: 1 } + ]); + }); + + it('should allow setting the ByLevelTreeGridMergeStrategy as the mergeStrategy to merge only data on the same hierarchy level.', () => { + grid.mergeStrategy = new ByLevelTreeGridMergeStrategy(); + fix.detectChanges(); + grid.triggerPipes(); + fix.detectChanges(); + const col = grid.getColumnByName('OnPTO'); + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: false, span: 1 }, + { value: false, span: 1 }, + { value: true, span: 1 }, + { value: false, span: 1 }, + { value: true, span: 1 }, + { value: false, span: 1 }, + { value: false, span: 1 }, + { value: true, span: 1 }, + { value: false, span: 1 }, + { value: false, span: 1 }, + { value: false, span: 1 }, + { value: true, span: 1 } + ]); + }); + }); }); }); From ea9baa684dfa247b46b8d2c76d43dd98e5460b9b Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 24 Jul 2025 14:43:14 +0300 Subject: [PATCH 050/106] chore(*): Fix lint i tests. --- .../src/lib/grids/grid/cell-merge.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts index 8e8bb8e58fb..0493bdef316 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -578,7 +578,9 @@ describe('IgxGrid - Cell merging #grid', () => { // check api expect(grid.getSelectedData().length).toBe(5); - expect(grid.getSelectedData()).toEqual(grid.data.slice(0, 5).map(x => { return { 'ID': x.ID, 'ProductName': x. ProductName}})); + expect(grid.getSelectedData()).toEqual(grid.data.slice(0, 5).map(x => { + return { 'ID': x.ID, 'ProductName': x. ProductName}; + })); }); }); @@ -603,7 +605,9 @@ describe('IgxGrid - Cell merging #grid', () => { col.selected = true; fix.detectChanges(); - expect(grid.getSelectedColumnsData()).toEqual(grid.data.map(x => { return {'ProductName': x. ProductName}})); + expect(grid.getSelectedColumnsData()).toEqual(grid.data.map(x => { + return {'ProductName': x. ProductName}; + })); }); }); From 1f4c5543152a7365aee0a474aff59ca7e0451710 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 24 Jul 2025 15:07:16 +0300 Subject: [PATCH 051/106] chore(*): Update Changelog. --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a5d8b720cf..1ea27757245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ All notable changes for each version of this project will be documented in this file. +## 20.1.0 + +### New Features + +- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid` + - Introduced a new cell merging feature that allows you to configure and merge cells in a column based on same data or other custom condition, into a single cell. + + It can be enabled on the individual columns: + + ```html + + ``` + The merging can be configured on the grid level to apply either: + - `onSort` - only when the column is sorted. + - `always` - always, regardless of data operations. + + ```html + + + ``` + + The default `cellMergeMode` is `onSort`. + + The functionality can be modified by setting a custom `mergeStrategy` on the grid, in case some other merge conditions or logic is needed for a custom scenario. + + It's possible also to set a `mergeComparer` on the individual columns, in case some custom handling is needed for a particular data field. + + ## 20.0.0 ### General From cc478207714eacca4dde0359dc7f89c2e99960af Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 24 Jul 2025 16:11:36 +0300 Subject: [PATCH 052/106] chore(*): Fix unrelated respy issue in combo tests. --- .../src/lib/simple-combo/simple-combo.component.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.spec.ts b/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.spec.ts index d14691194bb..f72b08c7292 100644 --- a/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.spec.ts +++ b/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.spec.ts @@ -532,6 +532,7 @@ describe('IgxSimpleCombo', () => { }); it('should delete the selection on destroy', () => { + jasmine.getEnv().allowRespy(true); const selectionService = new IgxSelectionAPIService(); const comboClearSpy = spyOn(mockComboService, 'clear'); const selectionDeleteSpy = spyOn(selectionService, 'delete'); @@ -548,6 +549,7 @@ describe('IgxSimpleCombo', () => { combo.ngOnDestroy(); expect(comboClearSpy).toHaveBeenCalled(); expect(selectionDeleteSpy).toHaveBeenCalled(); + jasmine.getEnv().allowRespy(false); }); }); From 35a1bde653e04b8f282497ce8198479d2a853525 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 24 Jul 2025 16:57:51 +0300 Subject: [PATCH 053/106] chore(*): Fix hardcoded value in unrelated test. --- .../src/lib/query-builder/query-builder.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8a1e2af8635..38f0578a17e 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 @@ -773,7 +773,7 @@ describe('IgxQueryBuilder', () => { // Verify value input placeholder const input = QueryBuilderFunctions.getQueryBuilderValueInput(fix).querySelector('input'); // Verify value input placeholder - expect(input.placeholder).toEqual('Select date'); + expect(input.placeholder).toEqual(queryBuilder.resourceStrings.igx_query_builder_date_placeholder); QueryBuilderFunctions.verifyEditModeExpressionInputStates(fix, true, true, false, true); // Third input should be disabled for unary operators. // Commit the populated expression. From a7970d126246a35165c2cbdcf61c48ffbd2485fb Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 25 Jul 2025 11:55:06 +0300 Subject: [PATCH 054/106] chore(*): Improve samples a bit. --- .../styles/components/grid/_grid-theme.scss | 2 + .../grid-cellMerging.component.html | 45 +- .../grid-cellMerging.component.scss | 3 + .../grid-cellMerging.component.ts | 343 +- src/app/shared/invoiceData.ts | 8081 +++++++++++++++++ 5 files changed, 8113 insertions(+), 361 deletions(-) create mode 100644 src/app/shared/invoiceData.ts diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index 5f5df28fc9c..4537aa86c68 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1178,6 +1178,7 @@ %grid-row--mrl { %igx-grid__hierarchical-expander--header, + %igx-grid__hierarchical-expander, %igx-grid__header-indentation, %igx-grid__row-indentation, %grid__cbx-selection { @@ -1310,6 +1311,7 @@ } %grid__cbx-selection, + %igx-grid__hierarchical-expander, %igx-grid__row-indentation, %igx-grid__drag-indicator { border-bottom: rem(1px) solid var-get($theme, 'row-border-color'); diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index b522b1af332..bc53e1cb9fa 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -1,7 +1,7 @@

Grid with cell merge

- + @if (searchText.length === 0) { search @@ -45,46 +45,47 @@

Grid with cell merge

- - -
- - - -
-
+ + + + + + + + + + + + + - + - + - + - + - + -

Hierarchical grid with cell merge

- - + @@ -108,7 +109,7 @@

Hierarchical grid with cell merge

Tree grid with cell merge

- diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.scss b/src/app/grid-cellMerging/grid-cellMerging.component.scss index 3d4037836d0..9f4f5d20867 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.scss +++ b/src/app/grid-cellMerging/grid-cellMerging.component.scss @@ -17,3 +17,6 @@ .grid-size { --ig-size: var(--ig-size-small); } +.searchInput{ + width: 800px; +} diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index 89b86f37a5e..d8cb3919a15 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -27,6 +27,7 @@ import { HIERARCHICAL_DATA } from '../shared/hierarchicalData'; import { data, dataWithoutPK } from '../shared/data'; import { HIERARCHICAL_SAMPLE_DATA } from '../shared/sample-data'; import { ByLevelTreeGridMergeStrategy } from 'igniteui-angular'; +import { INVOICE_DATA } from '../shared/invoiceData'; @Component({ selector: 'app-grid-cellMerging', @@ -36,16 +37,13 @@ import { ByLevelTreeGridMergeStrategy } from 'igniteui-angular'; FormsModule, IgxColumnComponent, IgxGridComponent, - IgxCellTemplateDirective, - IgxButtonDirective, IgxPaginatorComponent, - IgxActionStripComponent, - IgxGridPinningActionsComponent, + // IgxActionStripComponent, + // IgxGridPinningActionsComponent, IgxGridToolbarComponent, IgxGridToolbarActionsComponent, IgxGridToolbarPinningComponent, IgxGridToolbarHidingComponent, - IgxGridToolbarExporterComponent, IgxHierarchicalGridComponent, IgxRowIslandComponent, IgxTreeGridComponent, @@ -64,340 +62,7 @@ export class GridCellMergingComponent { public alignTop= { alignItems: "flex-start", paddingTop: "12px" }; public searchText: string =''; @ViewChild('grid1', { static: true }) public grid: IgxGridComponent; - public data = [{ - ProductID: 1, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '10 boxes x 20 bags', - UnitPrice: '18.0000', - UnitsInStock: 39, - UnitsOnOrder: 0, - ReorderLevel: 10.567, - Discontinued: false, - OrderDate: null, - OrderDate2: new Date(1991, 2, 12, 18, 40, 50).toISOString() - }, { - ProductID: 2, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 3, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 4, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '20.0000', - UnitsInStock: 20, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 5, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 6, - ProductName: 'Chang', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 7, - ProductName: 'Chang', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 8, - ProductName: 'Chang', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 30, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 9, - ProductName: 'Aniseed Syrup', - SupplierID: 1, - CategoryID: 2, - QuantityPerUnit: '12 - 550 ml bottles', - UnitPrice: '10.0000', - UnitsInStock: 13, - UnitsOnOrder: 70, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2006-03-17').toISOString(), - OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() - }, - { - ProductID: 10, - ProductName: 'Chang', - SupplierID: 1, - CategoryID: 2, - QuantityPerUnit: '12 - 550 ml bottles', - UnitPrice: '10.0000', - UnitsInStock: 13, - UnitsOnOrder: 70, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2006-03-17').toISOString(), - OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() - }, - { - ProductID: 11, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 2, - QuantityPerUnit: '12 - 550 ml bottles', - UnitPrice: '10.0000', - UnitsInStock: 13, - UnitsOnOrder: 70, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2006-03-17').toISOString(), - OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() - }, - { - ProductID: 12, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 2, - QuantityPerUnit: '12 - 550 ml bottles', - UnitPrice: '10.0000', - UnitsInStock: 12, - UnitsOnOrder: 70, - ReorderLevel: 30, - Discontinued: false, - OrderDate: new Date('2006-03-17').toISOString(), - OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() - }, - { - ProductID: 13, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '10 boxes x 20 bags', - UnitPrice: '18.0000', - UnitsInStock: 39, - UnitsOnOrder: 0, - ReorderLevel: 10.567, - Discontinued: false, - OrderDate: null, - OrderDate2: new Date(1991, 2, 12, 18, 40, 50).toISOString() - }, { - ProductID: 14, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 15, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 16, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '20.0000', - UnitsInStock: 20, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 17, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 18, - ProductName: 'Chang', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 19, - ProductName: 'Chang', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 20, - ProductName: 'Chang', - SupplierID: 1, - CategoryID: 1, - QuantityPerUnit: '24 - 12 oz bottles', - UnitPrice: '19.0000', - UnitsInStock: 17, - UnitsOnOrder: 40, - ReorderLevel: 30, - Discontinued: false, - OrderDate: new Date('2003-03-17').toISOString(), - OrderDate2: new Date('2003-03-17').toISOString() - }, - { - ProductID: 21, - ProductName: 'Aniseed Syrup', - SupplierID: 1, - CategoryID: 2, - QuantityPerUnit: '12 - 550 ml bottles', - UnitPrice: '10.0000', - UnitsInStock: 13, - UnitsOnOrder: 70, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2006-03-17').toISOString(), - OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() - }, - { - ProductID: 22, - ProductName: 'Chang', - SupplierID: 1, - CategoryID: 2, - QuantityPerUnit: '12 - 550 ml bottles', - UnitPrice: '10.0000', - UnitsInStock: 13, - UnitsOnOrder: 70, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2006-03-17').toISOString(), - OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() - }, - { - ProductID: 23, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 2, - QuantityPerUnit: '12 - 550 ml bottles', - UnitPrice: '10.0000', - UnitsInStock: 13, - UnitsOnOrder: 70, - ReorderLevel: 25, - Discontinued: false, - OrderDate: new Date('2006-03-17').toISOString(), - OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() - }, - { - ProductID: 24, - ProductName: 'Chai', - SupplierID: 1, - CategoryID: 2, - QuantityPerUnit: '12 - 550 ml bottles', - UnitPrice: '10.0000', - UnitsInStock: 12, - UnitsOnOrder: 70, - ReorderLevel: 30, - Discontinued: false, - OrderDate: new Date('2006-03-17').toISOString(), - OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString() - }]; + public data = INVOICE_DATA; public searchKeyDown(ev) { if (ev.key === 'Enter' || ev.key === 'ArrowDown' || ev.key === 'ArrowRight') { diff --git a/src/app/shared/invoiceData.ts b/src/app/shared/invoiceData.ts new file mode 100644 index 00000000000..051444c0009 --- /dev/null +++ b/src/app/shared/invoiceData.ts @@ -0,0 +1,8081 @@ +/* eslint-disable */ + +export interface Invoice { + ID: number; + ShipAddress: string; + ShipCity: string; + ShipCountry: string; + ShipName: string; + ShipRegion: string; + ShipPostalCode: string; + CustomerID: string; + CustomerName: string; + Address: string; + City: string; + Region: string; + PostalCode: string; + Country: string; + Salesperson: string; + OrderID: number; + OrderDate: Date; + ShipperName: string; + ProductID: number; + ProductName: string; + UnitPrice: number; + Quantity: number; + Discontinued: boolean; + ExtendedPrice: number; + Freight: number; +} + +export const INVOICE_DATA = [{ + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipCountry: "Germany", + ShipName: "Alfred's Futterkiste", + ShipRegion: null, + ShipPostalCode: "12209", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10692, + OrderDate: new Date("11/23/2016"), + ShipperName: "United Package", + ProductID: 63, + ProductName: "Vegie-spread", + UnitPrice: 43.9000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 878.0000, + Freight: 61.0200 +}, { + ShipName: "Alfred's Futterkiste", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10702, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 3, + ProductName: "Aniseed Syrup", + UnitPrice: 10.0000, + Quantity: 6, + Discontinued: false, + ExtendedPrice: 60.0000, + Freight: 23.9400 +}, { + ShipName: "Alfred's Futterkiste", + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10702, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 76, + ProductName: "Lakkalik\u00f6\u00f6ri", + UnitPrice: 18.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 270.0000, + Freight: 23.9400 +}, { + ShipName: "Alfred's Futterkiste", + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Nancy Davolio", + OrderID: 10835, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 59, + ProductName: "Raclette Courdavault", + UnitPrice: 55.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 825.0000, + Freight: 69.5300 +}, { + ShipName: "Alfred's Futterkiste", + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Nancy Davolio", + OrderID: 10952, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 28, + ProductName: "R\u00f6ssle Sauerkraut", + UnitPrice: 45.6000, + Quantity: 2, + Discontinued: true, + ExtendedPrice: 91.2000, + Freight: 40.4200 +}, { + ShipName: "Alfred's Futterkiste", + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Janet Leverling", + OrderID: 11011, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 71, + ProductName: "Flotemysost", + UnitPrice: 21.5000, + Quantity: 20, + Discontinued: true, + ExtendedPrice: 430.0000, + Freight: 1.2100 +}, { + ShipName: "Alfred's Futterkiste", + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Nancy Davolio", + OrderID: 10952, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 6, + ProductName: "Grandma's Boysenberry Spread", + UnitPrice: 25.0000, + Quantity: 16, + Discontinued: true, + ExtendedPrice: 380.0000, + Freight: 40.4200 +}, { + ShipName: "Alfred's Futterkiste", + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Janet Leverling", + OrderID: 11011, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 58, + ProductName: "Escargots de Bourgogne", + UnitPrice: 13.2500, + Quantity: 40, + Discontinued: true, + ExtendedPrice: 503.5000, + Freight: 1.2100 +}, { + ShipName: "Alfred's Futterkiste", + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Nancy Davolio", + OrderID: 10835, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 77, + ProductName: "Original Frankfurter gr\u00fcne So\u00dfe", + UnitPrice: 13.0000, + Quantity: 2, + Discontinued: true, + ExtendedPrice: 20.8000, + Freight: 69.5300 +}, { + ShipName: "Alfreds Futterkiste", + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Michael Suyama", + OrderID: 10643, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 28, + ProductName: "R\u00f6ssle Sauerkraut", + UnitPrice: 45.6000, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 513.0000, + Freight: 29.4600 +}, { + ShipName: "Alfreds Futterkiste", + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Michael Suyama", + OrderID: 10643, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 39, + ProductName: "Chartreuse verte", + UnitPrice: 18.0000, + Quantity: 21, + Discontinued: true, + ExtendedPrice: 283.5000, + Freight: 29.4600 +}, { + ShipName: "Alfreds Futterkiste", + ShipAddress: "Obere Str. 57", + ShipCity: "Berlin", + ShipRegion: null, + ShipPostalCode: "12209", + ShipCountry: "Germany", + CustomerID: "ALFKI", + CustomerName: "Alfreds Futterkiste", + Address: "Obere Str. 57", + City: "Berlin", + Region: null, + PostalCode: "12209", + Country: "Germany", + Salesperson: "Michael Suyama", + OrderID: 10643, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 46, + ProductName: "Spegesild", + UnitPrice: 12.0000, + Quantity: 2, + Discontinued: true, + ExtendedPrice: 18.0000, + Freight: 29.4600 +}, { + ShipName: "Ana Trujillo Emparedados y helados", + ShipAddress: "Avda. de la Constituci\u00f3n 2222", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05021", + ShipCountry: "Mexico", + CustomerID: "ANATR", + CustomerName: "Ana Trujillo Emparedados y helados", + Address: "Avda. de la Constituci\u00f3n 2222", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05021", + Country: "Mexico", + Salesperson: "Robert King", + OrderID: 10308, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 69, + ProductName: "Gudbrandsdalsost", + UnitPrice: 28.8000, + Quantity: 1, + Discontinued: false, + ExtendedPrice: 28.8000, + Freight: 1.6100 +}, { + ShipName: "Ana Trujillo Emparedados y helados", + ShipAddress: "Avda. de la Constituci\u00f3n 2222", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05021", + ShipCountry: "Mexico", + CustomerID: "ANATR", + CustomerName: "Ana Trujillo Emparedados y helados", + Address: "Avda. de la Constituci\u00f3n 2222", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05021", + Country: "Mexico", + Salesperson: "Robert King", + OrderID: 10308, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 70, + ProductName: "Outback Lager", + UnitPrice: 12.0000, + Quantity: 5, + Discontinued: false, + ExtendedPrice: 60.0000, + Freight: 1.6100 +}, { + ShipName: "Ana Trujillo Emparedados y helados", + ShipAddress: "Avda. de la Constituci\u00f3n 2222", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05021", + ShipCountry: "Mexico", + CustomerID: "ANATR", + CustomerName: "Ana Trujillo Emparedados y helados", + Address: "Avda. de la Constituci\u00f3n 2222", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05021", + Country: "Mexico", + Salesperson: "Janet Leverling", + OrderID: 10625, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 14, + ProductName: "Tofu", + UnitPrice: 23.2500, + Quantity: 3, + Discontinued: false, + ExtendedPrice: 69.7500, + Freight: 43.9000 +}, { + ShipName: "Ana Trujillo Emparedados y helados", + ShipAddress: "Avda. de la Constituci\u00f3n 2222", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05021", + ShipCountry: "Mexico", + CustomerID: "ANATR", + CustomerName: "Ana Trujillo Emparedados y helados", + Address: "Avda. de la Constituci\u00f3n 2222", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05021", + Country: "Mexico", + Salesperson: "Janet Leverling", + OrderID: 10625, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 42, + ProductName: "Singaporean Hokkien Fried Mee", + UnitPrice: 14.0000, + Quantity: 5, + Discontinued: false, + ExtendedPrice: 70.0000, + Freight: 43.9000 +}, { + ShipName: "Ana Trujillo Emparedados y helados", + ShipAddress: "Avda. de la Constituci\u00f3n 2222", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05021", + ShipCountry: "Mexico", + CustomerID: "ANATR", + CustomerName: "Ana Trujillo Emparedados y helados", + Address: "Avda. de la Constituci\u00f3n 2222", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05021", + Country: "Mexico", + Salesperson: "Janet Leverling", + OrderID: 10625, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 60, + ProductName: "Camembert Pierrot", + UnitPrice: 34.0000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 340.0000, + Freight: 43.9000 +}, { + ShipName: "Ana Trujillo Emparedados y helados", + ShipAddress: "Avda. de la Constituci\u00f3n 2222", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05021", + ShipCountry: "Mexico", + CustomerID: "ANATR", + CustomerName: "Ana Trujillo Emparedados y helados", + Address: "Avda. de la Constituci\u00f3n 2222", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05021", + Country: "Mexico", + Salesperson: "Janet Leverling", + OrderID: 10759, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 32, + ProductName: "Mascarpone Fabioli", + UnitPrice: 32.0000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 320.0000, + Freight: 11.9900 +}, { + ShipName: "Ana Trujillo Emparedados y helados", + ShipAddress: "Avda. de la Constituci\u00f3n 2222", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05021", + ShipCountry: "Mexico", + CustomerID: "ANATR", + CustomerName: "Ana Trujillo Emparedados y helados", + Address: "Avda. de la Constituci\u00f3n 2222", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05021", + Country: "Mexico", + Salesperson: "Margaret Peacock", + OrderID: 10926, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 11, + ProductName: "Queso Cabrales", + UnitPrice: 21.0000, + Quantity: 2, + Discontinued: false, + ExtendedPrice: 42.0000, + Freight: 39.9200 +}, { + ShipName: "Ana Trujillo Emparedados y helados", + ShipAddress: "Avda. de la Constituci\u00f3n 2222", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05021", + ShipCountry: "Mexico", + CustomerID: "ANATR", + CustomerName: "Ana Trujillo Emparedados y helados", + Address: "Avda. de la Constituci\u00f3n 2222", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05021", + Country: "Mexico", + Salesperson: "Margaret Peacock", + OrderID: 10926, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 13, + ProductName: "Konbu", + UnitPrice: 6.0000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 60.0000, + Freight: 39.9200 +}, { + ShipName: "Ana Trujillo Emparedados y helados", + ShipAddress: "Avda. de la Constituci\u00f3n 2222", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05021", + ShipCountry: "Mexico", + CustomerID: "ANATR", + CustomerName: "Ana Trujillo Emparedados y helados", + Address: "Avda. de la Constituci\u00f3n 2222", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05021", + Country: "Mexico", + Salesperson: "Margaret Peacock", + OrderID: 10926, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 19, + ProductName: "Teatime Chocolate Biscuits", + UnitPrice: 9.2000, + Quantity: 7, + Discontinued: false, + ExtendedPrice: 64.4000, + Freight: 39.9200 +}, { + ShipName: "Ana Trujillo Emparedados y helados", + ShipAddress: "Avda. de la Constituci\u00f3n 2222", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05021", + ShipCountry: "Mexico", + CustomerID: "ANATR", + CustomerName: "Ana Trujillo Emparedados y helados", + Address: "Avda. de la Constituci\u00f3n 2222", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05021", + Country: "Mexico", + Salesperson: "Margaret Peacock", + OrderID: 10926, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 72, + ProductName: "Mozzarella di Giovanni", + UnitPrice: 34.8000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 348.0000, + Freight: 39.9200 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Janet Leverling", + OrderID: 10365, + OrderDate: new Date("11/23/2016"), + ShipperName: "United Package", + ProductID: 11, + ProductName: "Queso Cabrales", + UnitPrice: 16.8000, + Quantity: 24, + Discontinued: false, + ExtendedPrice: 403.2000, + Freight: 22.0000 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Robert King", + OrderID: 10573, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 17, + ProductName: "Alice Mutton", + UnitPrice: 39.0000, + Quantity: 18, + Discontinued: false, + ExtendedPrice: 702.0000, + Freight: 84.8400 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Robert King", + OrderID: 10573, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 34, + ProductName: "Sasquatch Ale", + UnitPrice: 14.0000, + Quantity: 40, + Discontinued: false, + ExtendedPrice: 560.0000, + Freight: 84.8400 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Robert King", + OrderID: 10573, + OrderDate: new Date("11/23/2016"), + ShipperName: "Federal Shipping", + ProductID: 53, + ProductName: "Perth Pasties", + UnitPrice: 32.8000, + Quantity: 25, + Discontinued: false, + ExtendedPrice: 820.0000, + Freight: 84.8400 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Janet Leverling", + OrderID: 10682, + OrderDate: new Date("11/23/2016"), + ShipperName: "United Package", + ProductID: 33, + ProductName: "Geitost", + UnitPrice: 2.5000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 75.0000, + Freight: 36.1300 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Janet Leverling", + OrderID: 10682, + OrderDate: new Date("11/23/2016"), + ShipperName: "United Package", + ProductID: 66, + ProductName: "Louisiana Hot Spiced Okra", + UnitPrice: 17.0000, + Quantity: 4, + Discontinued: false, + ExtendedPrice: 68.0000, + Freight: 36.1300 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Janet Leverling", + OrderID: 10682, + OrderDate: new Date("11/23/2016"), + ShipperName: "United Package", + ProductID: 75, + ProductName: "Rh\u00f6nbr\u00e4u Klosterbier", + UnitPrice: 7.7500, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 232.5000, + Freight: 36.1300 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Janet Leverling", + OrderID: 10856, + OrderDate: new Date("11/23/2016"), + ShipperName: "United Package", + ProductID: 2, + ProductName: "Chang", + UnitPrice: 19.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 380.0000, + Freight: 58.4300 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Janet Leverling", + OrderID: 10856, + OrderDate: new Date("11/23/2016"), + ShipperName: "United Package", + ProductID: 42, + ProductName: "Singaporean Hokkien Fried Mee", + UnitPrice: 14.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 280.0000, + Freight: 58.4300 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Margaret Peacock", + OrderID: 10535, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 11, + ProductName: "Queso Cabrales", + UnitPrice: 21.0000, + Quantity: 50, + Discontinued: true, + ExtendedPrice: 945.0000, + Freight: 15.6400 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Margaret Peacock", + OrderID: 10535, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 40, + ProductName: "Boston Crab Meat", + UnitPrice: 18.4000, + Quantity: 10, + Discontinued: true, + ExtendedPrice: 165.6000, + Freight: 15.6400 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Margaret Peacock", + OrderID: 10535, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 57, + ProductName: "Ravioli Angelo", + UnitPrice: 19.5000, + Quantity: 5, + Discontinued: true, + ExtendedPrice: 87.7500, + Freight: 15.6400 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Margaret Peacock", + OrderID: 10535, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 59, + ProductName: "Raclette Courdavault", + UnitPrice: 55.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 742.5000, + Freight: 15.6400 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Robert King", + OrderID: 10507, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 43, + ProductName: "Ipoh Coffee", + UnitPrice: 46.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 586.5000, + Freight: 47.4500 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Robert King", + OrderID: 10507, + OrderDate: new Date("11/23/2016"), + ShipperName: "Speedy Express", + ProductID: 48, + ProductName: "Chocolade", + UnitPrice: 12.7500, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 162.5600, + Freight: 47.4500 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Nancy Davolio", + OrderID: 10677, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 26, + ProductName: "Gumb\u00e4r Gummib\u00e4rchen", + UnitPrice: 31.2300, + Quantity: 30, + Discontinued: true, + ExtendedPrice: 796.3700, + Freight: 4.0300 +}, { + ShipName: "Antonio Moreno Taquer\u00eda", + ShipAddress: "Mataderos 2312", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05023", + ShipCountry: "Mexico", + CustomerID: "ANTON", + CustomerName: "Antonio Moreno Taquer\u00eda", + Address: "Mataderos 2312", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05023", + Country: "Mexico", + Salesperson: "Nancy Davolio", + OrderID: 10677, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 33, + ProductName: "Geitost", + UnitPrice: 2.5000, + Quantity: 8, + Discontinued: false, + ExtendedPrice: 17.0000, + Freight: 4.0300 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Michael Suyama", + OrderID: 10355, + OrderDate: new Date("10/4/2017"), + ShipperName: "Speedy Express", + ProductID: 24, + ProductName: "Guaran\u00e1 Fant\u00e1stica", + UnitPrice: 3.6000, + Quantity: 25, + Discontinued: false, + ExtendedPrice: 90.0000, + Freight: 41.9500 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Michael Suyama", + OrderID: 10355, + OrderDate: new Date("10/4/2017"), + ShipperName: "Speedy Express", + ProductID: 57, + ProductName: "Ravioli Angelo", + UnitPrice: 15.6000, + Quantity: 25, + Discontinued: false, + ExtendedPrice: 390.0000, + Freight: 41.9500 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 10383, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 13, + ProductName: "Konbu", + UnitPrice: 4.8000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 96.0000, + Freight: 34.2400 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 10383, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 50, + ProductName: "Valkoinen suklaa", + UnitPrice: 13.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 195.0000, + Freight: 34.2400 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 10383, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 56, + ProductName: "Gnocchi di nonna Alice", + UnitPrice: 30.4000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 608.0000, + Freight: 34.2400 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10558, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 47, + ProductName: "Zaanse koeken", + UnitPrice: 9.5000, + Quantity: 25, + Discontinued: false, + ExtendedPrice: 237.5000, + Freight: 72.9700 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10558, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 51, + ProductName: "Manjimup Dried Apples", + UnitPrice: 53.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 1060.0000, + Freight: 72.9700 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10558, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 52, + ProductName: "Filo Mix", + UnitPrice: 7.0000, + Quantity: 30, + Discontinued: true, + ExtendedPrice: 210.0000, + Freight: 72.9700 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10558, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 53, + ProductName: "Perth Pasties", + UnitPrice: 32.8000, + Quantity: 18, + Discontinued: false, + ExtendedPrice: 590.4000, + Freight: 72.9700 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10558, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 73, + ProductName: "R\u00f6d Kaviar", + UnitPrice: 15.0000, + Quantity: 3, + Discontinued: false, + ExtendedPrice: 45.0000, + Freight: 72.9700 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 10707, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 55, + ProductName: "P\u00e2t\u00e9 chinois", + UnitPrice: 24.0000, + Quantity: 21, + Discontinued: false, + ExtendedPrice: 504.0000, + Freight: 21.7400 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 10707, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 57, + ProductName: "Ravioli Angelo", + UnitPrice: 19.5000, + Quantity: 40, + Discontinued: false, + ExtendedPrice: 780.0000, + Freight: 21.7400 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Janet Leverling", + OrderID: 10768, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 22, + ProductName: "Gustaf's Kn\u00e4ckebr\u00f6d", + UnitPrice: 21.0000, + Quantity: 4, + Discontinued: false, + ExtendedPrice: 84.0000, + Freight: 146.3200 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Janet Leverling", + OrderID: 10768, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 31, + ProductName: "Gorgonzola Telino", + UnitPrice: 12.5000, + Quantity: 50, + Discontinued: true, + ExtendedPrice: 625.0000, + Freight: 146.3200 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Janet Leverling", + OrderID: 10768, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 60, + ProductName: "Camembert Pierrot", + UnitPrice: 34.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 510.0000, + Freight: 146.3200 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Janet Leverling", + OrderID: 10768, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 71, + ProductName: "Flotemysost", + UnitPrice: 21.5000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 258.0000, + Freight: 146.3200 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Janet Leverling", + OrderID: 10793, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 41, + ProductName: "Jack's New England Clam Chowder", + UnitPrice: 9.6500, + Quantity: 14, + Discontinued: false, + ExtendedPrice: 135.1000, + Freight: 4.5200 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Janet Leverling", + OrderID: 10793, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 52, + ProductName: "Filo Mix", + UnitPrice: 7.0000, + Quantity: 8, + Discontinued: false, + ExtendedPrice: 56.0000, + Freight: 4.5200 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 10864, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 35, + ProductName: "Steeleye Stout", + UnitPrice: 18.0000, + Quantity: 4, + Discontinued: false, + ExtendedPrice: 72.0000, + Freight: 3.0400 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 10864, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 67, + ProductName: "Laughing Lumberjack Lager", + UnitPrice: 14.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 210.0000, + Freight: 3.0400 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 10920, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 50, + ProductName: "Valkoinen suklaa", + UnitPrice: 16.2500, + Quantity: 24, + Discontinued: false, + ExtendedPrice: 390.0000, + Freight: 29.6100 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Anne Dodsworth", + OrderID: 11016, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 31, + ProductName: "Gorgonzola Telino", + UnitPrice: 12.5000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 187.5000, + Freight: 33.8000 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Anne Dodsworth", + OrderID: 11016, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 36, + ProductName: "Inlagd Sill", + UnitPrice: 19.0000, + Quantity: 16, + Discontinued: false, + ExtendedPrice: 304.0000, + Freight: 33.8000 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10743, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 46, + ProductName: "Spegesild", + UnitPrice: 12.0000, + Quantity: 28, + Discontinued: false, + ExtendedPrice: 319.2000, + Freight: 23.7200 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Anne Dodsworth", + OrderID: 10953, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 20, + ProductName: "Sir Rodney's Marmalade", + UnitPrice: 81.0000, + Quantity: 50, + Discontinued: false, + ExtendedPrice: 3847.5000, + Freight: 23.7200 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Anne Dodsworth", + OrderID: 10953, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 31, + ProductName: "Gorgonzola Telino", + UnitPrice: 12.5000, + Quantity: 50, + Discontinued: false, + ExtendedPrice: 593.7500, + Freight: 23.7200 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10453, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 48, + ProductName: "Chocolade", + UnitPrice: 10.2000, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 137.7000, + Freight: 25.3600 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10453, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 70, + ProductName: "Outback Lager", + UnitPrice: 12.0000, + Quantity: 25, + Discontinued: true, + ExtendedPrice: 270.0000, + Freight: 25.3600 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 10707, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 70, + ProductName: "Outback Lager", + UnitPrice: 15.0000, + Quantity: 28, + Discontinued: false, + ExtendedPrice: 357.0000, + Freight: 21.7400 +}, { + ShipName: "Around the Horn", + ShipAddress: "Brook Farm Stratford St. Mary", + ShipCity: "Colchester", + ShipRegion: "Essex", + ShipPostalCode: "CO7 6JX", + ShipCountry: "UK", + CustomerID: "AROUT", + CustomerName: "Around the Horn", + Address: "120 Hanover Sq.", + City: "London", + Region: null, + PostalCode: "WA1 1DP", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 10741, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 2, + ProductName: "Chang", + UnitPrice: 19.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 228.0000, + Freight: 10.9600 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Laura Callahan", + OrderID: 10278, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 44, + ProductName: "Gula Malacca", + UnitPrice: 15.5000, + Quantity: 16, + Discontinued: false, + ExtendedPrice: 248.0000, + Freight: 92.6900 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Laura Callahan", + OrderID: 10278, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 59, + ProductName: "Raclette Courdavault", + UnitPrice: 44.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 660.0000, + Freight: 92.6900 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Laura Callahan", + OrderID: 10278, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 63, + ProductName: "Vegie-spread", + UnitPrice: 35.1000, + Quantity: 8, + Discontinued: false, + ExtendedPrice: 280.8000, + Freight: 92.6900 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Laura Callahan", + OrderID: 10278, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 73, + ProductName: "R\u00f6d Kaviar", + UnitPrice: 12.0000, + Quantity: 25, + Discontinued: false, + ExtendedPrice: 300.0000, + Freight: 92.6900 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Andrew Fuller", + OrderID: 10280, + OrderDate: new Date("10/4/2017"), + ShipperName: "Speedy Express", + ProductID: 24, + ProductName: "Guaran\u00e1 Fant\u00e1stica", + UnitPrice: 3.6000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 43.2000, + Freight: 8.9800 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Andrew Fuller", + OrderID: 10280, + OrderDate: new Date("10/4/2017"), + ShipperName: "Speedy Express", + ProductID: 55, + ProductName: "P\u00e2t\u00e9 chinois", + UnitPrice: 19.2000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 384.0000, + Freight: 8.9800 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Andrew Fuller", + OrderID: 10280, + OrderDate: new Date("10/4/2017"), + ShipperName: "Speedy Express", + ProductID: 75, + ProductName: "Rh\u00f6nbr\u00e4u Klosterbier", + UnitPrice: 6.2000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 186.0000, + Freight: 8.9800 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10384, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 20, + ProductName: "Sir Rodney's Marmalade", + UnitPrice: 64.8000, + Quantity: 28, + Discontinued: false, + ExtendedPrice: 1814.4000, + Freight: 168.6400 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10384, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 60, + ProductName: "Camembert Pierrot", + UnitPrice: 27.2000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 408.0000, + Freight: 168.6400 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10444, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 17, + ProductName: "Alice Mutton", + UnitPrice: 31.2000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 312.0000, + Freight: 3.5000 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10444, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 26, + ProductName: "Gumb\u00e4r Gummib\u00e4rchen", + UnitPrice: 24.9000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 373.5000, + Freight: 3.5000 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10444, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 35, + ProductName: "Steeleye Stout", + UnitPrice: 14.4000, + Quantity: 8, + Discontinued: false, + ExtendedPrice: 115.2000, + Freight: 3.5000 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10444, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 41, + ProductName: "Jack's New England Clam Chowder", + UnitPrice: 7.7000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 231.0000, + Freight: 3.5000 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Nancy Davolio", + OrderID: 10524, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 54, + ProductName: "Tourti\u00e8re", + UnitPrice: 7.4500, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 111.7500, + Freight: 244.7900 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10572, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 40, + ProductName: "Boston Crab Meat", + UnitPrice: 18.4000, + Quantity: 50, + Discontinued: false, + ExtendedPrice: 920.0000, + Freight: 116.4300 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Nancy Davolio", + OrderID: 10626, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 53, + ProductName: "Perth Pasties", + UnitPrice: 32.8000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 393.6000, + Freight: 138.6900 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Nancy Davolio", + OrderID: 10626, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 60, + ProductName: "Camembert Pierrot", + UnitPrice: 34.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 680.0000, + Freight: 138.6900 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Nancy Davolio", + OrderID: 10626, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 71, + ProductName: "Flotemysost", + UnitPrice: 21.5000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 430.0000, + Freight: 138.6900 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Anne Dodsworth", + OrderID: 10672, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 71, + ProductName: "Flotemysost", + UnitPrice: 21.5000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 258.0000, + Freight: 95.7500 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Nancy Davolio", + OrderID: 10733, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 14, + ProductName: "Tofu", + UnitPrice: 23.2500, + Quantity: 16, + Discontinued: false, + ExtendedPrice: 372.0000, + Freight: 110.1100 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Nancy Davolio", + OrderID: 10733, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 28, + ProductName: "R\u00f6ssle Sauerkraut", + UnitPrice: 45.6000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 912.0000, + Freight: 110.1100 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Nancy Davolio", + OrderID: 10733, + OrderDate: new Date("10/4/2017"), + ShipperName: "Federal Shipping", + ProductID: 52, + ProductName: "Filo Mix", + UnitPrice: 7.0000, + Quantity: 25, + Discontinued: false, + ExtendedPrice: 175.0000, + Freight: 110.1100 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10778, + OrderDate: new Date("10/4/2017"), + ShipperName: "Speedy Express", + ProductID: 41, + ProductName: "Jack's New England Clam Chowder", + UnitPrice: 9.6500, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 96.5000, + Freight: 6.7900 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Anne Dodsworth", + OrderID: 10837, + OrderDate: new Date("10/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 13, + ProductName: "Konbu", + UnitPrice: 6.0000, + Quantity: 6, + Discontinued: false, + ExtendedPrice: 36.0000, + Freight: 13.3200 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Anne Dodsworth", + OrderID: 10837, + OrderDate: new Date("10/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 40, + ProductName: "Boston Crab Meat", + UnitPrice: 18.4000, + Quantity: 25, + Discontinued: false, + ExtendedPrice: 460.0000, + Freight: 13.3200 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Laura Callahan", + OrderID: 10857, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 3, + ProductName: "Aniseed Syrup", + UnitPrice: 10.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 300.0000, + Freight: 188.8500 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Margaret Peacock", + OrderID: 10875, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 19, + ProductName: "Teatime Chocolate Biscuits", + UnitPrice: 9.2000, + Quantity: 25, + Discontinued: false, + ExtendedPrice: 230.0000, + Freight: 32.3700 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Margaret Peacock", + OrderID: 10875, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 49, + ProductName: "Maxilaku", + UnitPrice: 20.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 300.0000, + Freight: 32.3700 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10924, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 75, + ProductName: "Rh\u00f6nbr\u00e4u Klosterbier", + UnitPrice: 7.7500, + Quantity: 6, + Discontinued: false, + ExtendedPrice: 46.5000, + Freight: 151.5200 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10572, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 16, + ProductName: "Pavlova", + UnitPrice: 17.4500, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 188.4600, + Freight: 116.4300 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10572, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 32, + ProductName: "Mascarpone Fabioli", + UnitPrice: 32.0000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 288.0000, + Freight: 116.4300 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10572, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 75, + ProductName: "Rh\u00f6nbr\u00e4u Klosterbier", + UnitPrice: 7.7500, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 104.6200, + Freight: 116.4300 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Steven Buchanan", + OrderID: 10654, + OrderDate: new Date("10/4/2018"), + ShipperName: "Speedy Express", + ProductID: 4, + ProductName: "Chef Anton's Cajun Seasoning", + UnitPrice: 22.0000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 237.6000, + Freight: 55.2600 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Steven Buchanan", + OrderID: 10654, + OrderDate: new Date("10/4/2018"), + ShipperName: "Speedy Express", + ProductID: 39, + ProductName: "Chartreuse verte", + UnitPrice: 18.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 324.0000, + Freight: 55.2600 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Steven Buchanan", + OrderID: 10654, + OrderDate: new Date("10/4/2018"), + ShipperName: "Speedy Express", + ProductID: 54, + ProductName: "Tourti\u00e8re", + UnitPrice: 7.4500, + Quantity: 6, + Discontinued: false, + ExtendedPrice: 40.2300, + Freight: 55.2600 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Anne Dodsworth", + OrderID: 10672, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 38, + ProductName: "C\u00f4te de Blaye", + UnitPrice: 263.5000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 3557.2500, + Freight: 95.7500 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Margaret Peacock", + OrderID: 10875, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 47, + ProductName: "Zaanse koeken", + UnitPrice: 9.5000, + Quantity: 21, + Discontinued: false, + ExtendedPrice: 179.5500, + Freight: 32.3700 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10924, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 10, + ProductName: "Ikura", + UnitPrice: 31.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 558.0000, + Freight: 151.5200 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Janet Leverling", + OrderID: 10924, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 28, + ProductName: "R\u00f6ssle Sauerkraut", + UnitPrice: 45.6000, + Quantity: 30, + Discontinued: true, + ExtendedPrice: 1231.2000, + Freight: 151.5200 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Nancy Davolio", + OrderID: 10689, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 1, + ProductName: "Chai", + UnitPrice: 18.0000, + Quantity: 35, + Discontinued: true, + ExtendedPrice: 472.5000, + Freight: 13.4200 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Anne Dodsworth", + OrderID: 10837, + OrderDate: new Date("10/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 47, + ProductName: "Zaanse koeken", + UnitPrice: 9.5000, + Quantity: 40, + Discontinued: true, + ExtendedPrice: 285.0000, + Freight: 13.3200 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Anne Dodsworth", + OrderID: 10837, + OrderDate: new Date("10/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 76, + ProductName: "Lakkalik\u00f6\u00f6ri", + UnitPrice: 18.0000, + Quantity: 21, + Discontinued: true, + ExtendedPrice: 283.5000, + Freight: 13.3200 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Laura Callahan", + OrderID: 10857, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 26, + ProductName: "Gumb\u00e4r Gummib\u00e4rchen", + UnitPrice: 31.2300, + Quantity: 35, + Discontinued: true, + ExtendedPrice: 819.7900, + Freight: 188.8500 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Laura Callahan", + OrderID: 10857, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 29, + ProductName: "Th\u00fcringer Rostbratwurst", + UnitPrice: 123.7900, + Quantity: 10, + Discontinued: true, + ExtendedPrice: 928.4300, + Freight: 188.8500 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Steven Buchanan", + OrderID: 10866, + OrderDate: new Date("10/4/2018"), + ShipperName: "Speedy Express", + ProductID: 2, + ProductName: "Chang", + UnitPrice: 19.0000, + Quantity: 21, + Discontinued: true, + ExtendedPrice: 299.2500, + Freight: 109.1100 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Steven Buchanan", + OrderID: 10866, + OrderDate: new Date("10/4/2018"), + ShipperName: "Speedy Express", + ProductID: 24, + ProductName: "Guaran\u00e1 Fant\u00e1stica", + UnitPrice: 4.5000, + Quantity: 6, + Discontinued: true, + ExtendedPrice: 20.2500, + Freight: 109.1100 +}, { + ShipName: "Berglunds snabbk\u00f6p", + ShipAddress: "Berguvsv\u00e4gen 8", + ShipCity: "Lule\u00e5", + ShipRegion: null, + ShipPostalCode: "S-958 22", + ShipCountry: "Sweden", + CustomerID: "BERGS", + CustomerName: "Berglunds snabbk\u00f6p", + Address: "Berguvsv\u00e4gen 8", + City: "Lule\u00e5", + Region: null, + PostalCode: "S-958 22", + Country: "Sweden", + Salesperson: "Steven Buchanan", + OrderID: 10866, + OrderDate: new Date("10/4/2018"), + ShipperName: "Speedy Express", + ProductID: 30, + ProductName: "Nord-Ost Matjeshering", + UnitPrice: 25.8900, + Quantity: 40, + Discontinued: false, + ExtendedPrice: 776.7000, + Freight: 109.1100 +}, { + ShipName: "Blauer See Delikatessen", + ShipAddress: "Forsterstr. 57", + ShipCity: "Mannheim", + ShipRegion: null, + ShipPostalCode: "68306", + ShipCountry: "Germany", + CustomerID: "BLAUS", + CustomerName: "Blauer See Delikatessen", + Address: "Forsterstr. 57", + City: "Mannheim", + Region: null, + PostalCode: "68306", + Country: "Germany", + Salesperson: "Anne Dodsworth", + OrderID: 10501, + OrderDate: new Date("10/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 54, + ProductName: "Tourti\u00e8re", + UnitPrice: 7.4500, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 149.0000, + Freight: 8.8500 +}, { + ShipName: "Blauer See Delikatessen", + ShipAddress: "Forsterstr. 57", + ShipCity: "Mannheim", + ShipRegion: null, + ShipPostalCode: "68306", + ShipCountry: "Germany", + CustomerID: "BLAUS", + CustomerName: "Blauer See Delikatessen", + Address: "Forsterstr. 57", + City: "Mannheim", + Region: null, + PostalCode: "68306", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10509, + OrderDate: new Date("10/4/2018"), + ShipperName: "Speedy Express", + ProductID: 28, + ProductName: "R\u00f6ssle Sauerkraut", + UnitPrice: 45.6000, + Quantity: 3, + Discontinued: false, + ExtendedPrice: 136.8000, + Freight: 0.1500 +}, { + ShipName: "Blauer See Delikatessen", + ShipAddress: "Forsterstr. 57", + ShipCity: "Mannheim", + ShipRegion: null, + ShipPostalCode: "68306", + ShipCountry: "Germany", + CustomerID: "BLAUS", + CustomerName: "Blauer See Delikatessen", + Address: "Forsterstr. 57", + City: "Mannheim", + Region: null, + PostalCode: "68306", + Country: "Germany", + Salesperson: "Janet Leverling", + OrderID: 10582, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 57, + ProductName: "Ravioli Angelo", + UnitPrice: 19.5000, + Quantity: 4, + Discontinued: false, + ExtendedPrice: 78.0000, + Freight: 27.7100 +}, { + ShipName: "Blauer See Delikatessen", + ShipAddress: "Forsterstr. 57", + ShipCity: "Mannheim", + ShipRegion: null, + ShipPostalCode: "68306", + ShipCountry: "Germany", + CustomerID: "BLAUS", + CustomerName: "Blauer See Delikatessen", + Address: "Forsterstr. 57", + City: "Mannheim", + Region: null, + PostalCode: "68306", + Country: "Germany", + Salesperson: "Janet Leverling", + OrderID: 10582, + OrderDate: new Date("10/4/2018"), + ShipperName: "United Package", + ProductID: 76, + ProductName: "Lakkalik\u00f6\u00f6ri", + UnitPrice: 18.0000, + Quantity: 14, + Discontinued: false, + ExtendedPrice: 252.0000, + Freight: 27.7100 +}, { + ShipName: "Blauer See Delikatessen", + ShipAddress: "Forsterstr. 57", + ShipCity: "Mannheim", + ShipRegion: null, + ShipPostalCode: "68306", + ShipCountry: "Germany", + CustomerID: "BLAUS", + CustomerName: "Blauer See Delikatessen", + Address: "Forsterstr. 57", + City: "Mannheim", + Region: null, + PostalCode: "68306", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 10614, + OrderDate: new Date("10/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 11, + ProductName: "Queso Cabrales", + UnitPrice: 21.0000, + Quantity: 14, + Discontinued: false, + ExtendedPrice: 294.0000, + Freight: 1.9300 +}, { + ShipName: "Blauer See Delikatessen", + ShipAddress: "Forsterstr. 57", + ShipCity: "Mannheim", + ShipRegion: null, + ShipPostalCode: "68306", + ShipCountry: "Germany", + CustomerID: "BLAUS", + CustomerName: "Blauer See Delikatessen", + Address: "Forsterstr. 57", + City: "Mannheim", + Region: null, + PostalCode: "68306", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 10614, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 21, + ProductName: "Sir Rodney's Scones", + UnitPrice: 10.0000, + Quantity: 8, + Discontinued: false, + ExtendedPrice: 80.0000, + Freight: 1.9300 +}, { + ShipName: "Blauer See Delikatessen", + ShipAddress: "Forsterstr. 57", + ShipCity: "Mannheim", + ShipRegion: null, + ShipPostalCode: "68306", + ShipCountry: "Germany", + CustomerID: "BLAUS", + CustomerName: "Blauer See Delikatessen", + Address: "Forsterstr. 57", + City: "Mannheim", + Region: null, + PostalCode: "68306", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 10614, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 39, + ProductName: "Chartreuse verte", + UnitPrice: 18.0000, + Quantity: 5, + Discontinued: false, + ExtendedPrice: 90.0000, + Freight: 1.9300 +}, { + ShipName: "Blauer See Delikatessen", + ShipAddress: "Forsterstr. 57", + ShipCity: "Mannheim", + ShipRegion: null, + ShipPostalCode: "68306", + ShipCountry: "Germany", + CustomerID: "BLAUS", + CustomerName: "Blauer See Delikatessen", + Address: "Forsterstr. 57", + City: "Mannheim", + Region: null, + PostalCode: "68306", + Country: "Germany", + Salesperson: "Anne Dodsworth", + OrderID: 10853, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 18, + ProductName: "Carnarvon Tigers", + UnitPrice: 62.5000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 625.0000, + Freight: 53.8300 +}, { + ShipName: "Blauer See Delikatessen", + ShipAddress: "Forsterstr. 57", + ShipCity: "Mannheim", + ShipRegion: null, + ShipPostalCode: "68306", + ShipCountry: "Germany", + CustomerID: "BLAUS", + CustomerName: "Blauer See Delikatessen", + Address: "Forsterstr. 57", + City: "Mannheim", + Region: null, + PostalCode: "68306", + Country: "Germany", + Salesperson: "Michael Suyama", + OrderID: 10956, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 21, + ProductName: "Sir Rodney's Scones", + UnitPrice: 10.0000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 120.0000, + Freight: 44.6500 +}, { + ShipName: "Blauer See Delikatessen", + ShipAddress: "Forsterstr. 57", + ShipCity: "Mannheim", + ShipRegion: null, + ShipPostalCode: "68306", + ShipCountry: "Germany", + CustomerID: "BLAUS", + CustomerName: "Blauer See Delikatessen", + Address: "Forsterstr. 57", + City: "Mannheim", + Region: null, + PostalCode: "68306", + Country: "Germany", + Salesperson: "Michael Suyama", + OrderID: 10956, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 47, + ProductName: "Zaanse koeken", + UnitPrice: 9.5000, + Quantity: 14, + Discontinued: false, + ExtendedPrice: 133.0000, + Freight: 44.6500 +}, { + ShipName: "B\u00f3lido Comidas preparadas", + ShipAddress: "C/ Araquil, 67", + ShipCity: "Madrid", + ShipRegion: null, + ShipPostalCode: "28023", + ShipCountry: "Spain", + CustomerID: "BOLID", + CustomerName: "B\u00f3lido Comidas preparadas", + Address: "C/ Araquil, 67", + City: "Madrid", + Region: null, + PostalCode: "28023", + Country: "Spain", + Salesperson: "Margaret Peacock", + OrderID: 10326, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 75, + ProductName: "Rh\u00f6nbr\u00e4u Klosterbier", + UnitPrice: 6.2000, + Quantity: 50, + Discontinued: false, + ExtendedPrice: 310.0000, + Freight: 77.9200 +}, { + ShipName: "B\u00f3lido Comidas preparadas", + ShipAddress: "C/ Araquil, 67", + ShipCity: "Madrid", + ShipRegion: null, + ShipPostalCode: "28023", + ShipCountry: "Spain", + CustomerID: "BOLID", + CustomerName: "B\u00f3lido Comidas preparadas", + Address: "C/ Araquil, 67", + City: "Madrid", + Region: null, + PostalCode: "28023", + Country: "Spain", + Salesperson: "Anne Dodsworth", + OrderID: 10970, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 52, + ProductName: "Filo Mix", + UnitPrice: 7.0000, + Quantity: 40, + Discontinued: false, + ExtendedPrice: 224.0000, + Freight: 16.1600 +}, { + ShipName: "B\u00f3lido Comidas preparadas", + ShipAddress: "C/ Araquil, 67", + ShipCity: "Madrid", + ShipRegion: null, + ShipPostalCode: "28023", + ShipCountry: "Spain", + CustomerID: "BOLID", + CustomerName: "B\u00f3lido Comidas preparadas", + Address: "C/ Araquil, 67", + City: "Madrid", + Region: null, + PostalCode: "28023", + Country: "Spain", + Salesperson: "Margaret Peacock", + OrderID: 10801, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 17, + ProductName: "Alice Mutton", + UnitPrice: 39.0000, + Quantity: 40, + Discontinued: false, + ExtendedPrice: 1170.0000, + Freight: 97.0900 +}, { + ShipName: "B\u00f3lido Comidas preparadas", + ShipAddress: "C/ Araquil, 67", + ShipCity: "Madrid", + ShipRegion: null, + ShipPostalCode: "28023", + ShipCountry: "Spain", + CustomerID: "BOLID", + CustomerName: "B\u00f3lido Comidas preparadas", + Address: "C/ Araquil, 67", + City: "Madrid", + Region: null, + PostalCode: "28023", + Country: "Spain", + Salesperson: "Margaret Peacock", + OrderID: 10801, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 29, + ProductName: "Th\u00fcringer Rostbratwurst", + UnitPrice: 123.7900, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 1856.8500, + Freight: 97.0900 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Anne Dodsworth", + OrderID: 10331, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 54, + ProductName: "Tourti\u00e8re", + UnitPrice: 5.9000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 88.5000, + Freight: 10.1900 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Janet Leverling", + OrderID: 10362, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 25, + ProductName: "NuNuCa Nu\u00df-Nougat-Creme", + UnitPrice: 11.2000, + Quantity: 50, + Discontinued: false, + ExtendedPrice: 560.0000, + Freight: 96.0400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Janet Leverling", + OrderID: 10362, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 51, + ProductName: "Manjimup Dried Apples", + UnitPrice: 42.4000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 848.0000, + Freight: 96.0400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Janet Leverling", + OrderID: 10362, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 54, + ProductName: "Tourti\u00e8re", + UnitPrice: 5.9000, + Quantity: 24, + Discontinued: false, + ExtendedPrice: 141.6000, + Freight: 96.0400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 10470, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 18, + ProductName: "Carnarvon Tigers", + UnitPrice: 50.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 1500.0000, + Freight: 64.5600 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 10470, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 23, + ProductName: "Tunnbr\u00f6d", + UnitPrice: 7.2000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 108.0000, + Freight: 64.5600 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 10470, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 64, + ProductName: "Wimmers gute Semmelkn\u00f6del", + UnitPrice: 26.6000, + Quantity: 8, + Discontinued: false, + ExtendedPrice: 212.8000, + Freight: 64.5600 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Nancy Davolio", + OrderID: 10525, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 36, + ProductName: "Inlagd Sill", + UnitPrice: 19.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 570.0000, + Freight: 11.0600 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Janet Leverling", + OrderID: 10715, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 10, + ProductName: "Ikura", + UnitPrice: 31.0000, + Quantity: 21, + Discontinued: false, + ExtendedPrice: 651.0000, + Freight: 63.2000 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Janet Leverling", + OrderID: 10715, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 71, + ProductName: "Flotemysost", + UnitPrice: 21.5000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 645.0000, + Freight: 63.2000 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Janet Leverling", + OrderID: 10732, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 76, + ProductName: "Lakkalik\u00f6\u00f6ri", + UnitPrice: 18.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 360.0000, + Freight: 16.9700 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Nancy Davolio", + OrderID: 10827, + OrderDate: new Date("10/4/2017"), + ShipperName: "United Package", + ProductID: 10, + ProductName: "Ikura", + UnitPrice: 31.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 465.0000, + Freight: 63.5400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Nancy Davolio", + OrderID: 10827, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 39, + ProductName: "Chartreuse verte", + UnitPrice: 18.0000, + Quantity: 21, + Discontinued: false, + ExtendedPrice: 378.0000, + Freight: 63.5400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Robert King", + OrderID: 10876, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 46, + ProductName: "Spegesild", + UnitPrice: 12.0000, + Quantity: 21, + Discontinued: false, + ExtendedPrice: 252.0000, + Freight: 60.4200 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Robert King", + OrderID: 10876, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 64, + ProductName: "Wimmers gute Semmelkn\u00f6del", + UnitPrice: 33.2500, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 665.0000, + Freight: 60.4200 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Laura Callahan", + OrderID: 10932, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 72, + ProductName: "Mozzarella di Giovanni", + UnitPrice: 34.8000, + Quantity: 16, + Discontinued: false, + ExtendedPrice: 556.8000, + Freight: 134.6400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Laura Callahan", + OrderID: 10940, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 13, + ProductName: "Konbu", + UnitPrice: 6.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 120.0000, + Freight: 19.7700 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Nancy Davolio", + OrderID: 10340, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 18, + ProductName: "Carnarvon Tigers", + UnitPrice: 50.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 950.0000, + Freight: 166.3100 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Nancy Davolio", + OrderID: 10340, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 41, + ProductName: "Jack's New England Clam Chowder", + UnitPrice: 7.7000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 87.7800, + Freight: 166.3100 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Nancy Davolio", + OrderID: 10340, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 43, + ProductName: "Ipoh Coffee", + UnitPrice: 36.8000, + Quantity: 40, + Discontinued: false, + ExtendedPrice: 1398.4000, + Freight: 166.3100 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Andrew Fuller", + OrderID: 10663, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 40, + ProductName: "Boston Crab Meat", + UnitPrice: 18.4000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 524.4000, + Freight: 113.1500 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Andrew Fuller", + OrderID: 10663, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 42, + ProductName: "Singaporean Hokkien Fried Mee", + UnitPrice: 14.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 399.0000, + Freight: 113.1500 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Andrew Fuller", + OrderID: 10663, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 51, + ProductName: "Manjimup Dried Apples", + UnitPrice: 53.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 1007.0000, + Freight: 113.1500 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Steven Buchanan", + OrderID: 10730, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 16, + ProductName: "Pavlova", + UnitPrice: 17.4500, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 248.6600, + Freight: 20.1200 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Steven Buchanan", + OrderID: 10730, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 31, + ProductName: "Gorgonzola Telino", + UnitPrice: 12.5000, + Quantity: 3, + Discontinued: false, + ExtendedPrice: 35.6200, + Freight: 20.1200 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Steven Buchanan", + OrderID: 10730, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 65, + ProductName: "Louisiana Fiery Hot Pepper Sauce", + UnitPrice: 21.0500, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 199.9700, + Freight: 20.1200 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Anne Dodsworth", + OrderID: 10871, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 6, + ProductName: "Grandma's Boysenberry Spread", + UnitPrice: 25.0000, + Quantity: 50, + Discontinued: false, + ExtendedPrice: 1187.5000, + Freight: 112.2700 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Anne Dodsworth", + OrderID: 10871, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 16, + ProductName: "Pavlova", + UnitPrice: 17.4500, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 198.9300, + Freight: 112.2700 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Anne Dodsworth", + OrderID: 10871, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 17, + ProductName: "Alice Mutton", + UnitPrice: 39.0000, + Quantity: 16, + Discontinued: false, + ExtendedPrice: 592.8000, + Freight: 112.2700 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Nancy Davolio", + OrderID: 10525, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 40, + ProductName: "Boston Crab Meat", + UnitPrice: 18.4000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 248.4000, + Freight: 11.0600 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Laura Callahan", + OrderID: 10932, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 16, + ProductName: "Pavlova", + UnitPrice: 17.4500, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 471.1500, + Freight: 134.6400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Laura Callahan", + OrderID: 10932, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 62, + ProductName: "Tarte au sucre", + UnitPrice: 49.3000, + Quantity: 14, + Discontinued: true, + ExtendedPrice: 621.1800, + Freight: 134.6400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Laura Callahan", + OrderID: 10932, + OrderDate: new Date("10/4/2016"), + ShipperName: "Speedy Express", + ProductID: 75, + ProductName: "Rh\u00f6nbr\u00e4u Klosterbier", + UnitPrice: 7.7500, + Quantity: 20, + Discontinued: true, + ExtendedPrice: 139.5000, + Freight: 134.6400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 10511, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 4, + ProductName: "Chef Anton's Cajun Seasoning", + UnitPrice: 22.0000, + Quantity: 50, + Discontinued: true, + ExtendedPrice: 935.0000, + Freight: 350.6400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 10511, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 7, + ProductName: "Uncle Bob's Organic Dried Pears", + UnitPrice: 30.0000, + Quantity: 50, + Discontinued: true, + ExtendedPrice: 1275.0000, + Freight: 350.6400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 10511, + OrderDate: new Date("10/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 8, + ProductName: "Northwoods Cranberry Sauce", + UnitPrice: 40.0000, + Quantity: 10, + Discontinued: true, + ExtendedPrice: 340.0000, + Freight: 350.6400 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 10755, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 47, + ProductName: "Zaanse koeken", + UnitPrice: 9.5000, + Quantity: 30, + Discontinued: true, + ExtendedPrice: 213.7500, + Freight: 16.7100 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 10755, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 56, + ProductName: "Gnocchi di nonna Alice", + UnitPrice: 38.0000, + Quantity: 30, + Discontinued: true, + ExtendedPrice: 855.0000, + Freight: 16.7100 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 10755, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 57, + ProductName: "Ravioli Angelo", + UnitPrice: 19.5000, + Quantity: 14, + Discontinued: true, + ExtendedPrice: 204.7500, + Freight: 16.7100 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 10755, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 69, + ProductName: "Gudbrandsdalsost", + UnitPrice: 36.0000, + Quantity: 25, + Discontinued: true, + ExtendedPrice: 675.0000, + Freight: 16.7100 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 11076, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 6, + ProductName: "Grandma's Boysenberry Spread", + UnitPrice: 25.0000, + Quantity: 20, + Discontinued: true, + ExtendedPrice: 375.0000, + Freight: 38.2800 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 11076, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 14, + ProductName: "Tofu", + UnitPrice: 23.2500, + Quantity: 20, + Discontinued: true, + ExtendedPrice: 348.7500, + Freight: 38.2800 +}, { + ShipName: "Bon app'", + ShipAddress: "12, rue des Bouchers", + ShipCity: "Marseille", + ShipRegion: null, + ShipPostalCode: "13008", + ShipCountry: "France", + CustomerID: "BONAP", + CustomerName: "Bon app'", + Address: "12, rue des Bouchers", + City: "Marseille", + Region: null, + PostalCode: "13008", + Country: "France", + Salesperson: "Margaret Peacock", + OrderID: 11076, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 19, + ProductName: "Teatime Chocolate Biscuits", + UnitPrice: 9.2000, + Quantity: 10, + Discontinued: true, + ExtendedPrice: 69.0000, + Freight: 38.2800 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Toronto Blvd.", + ShipCity: "Toronto", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Toronto Blvd.", + City: "Toronto", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Margaret Peacock", + OrderID: 10389, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 10, + ProductName: "Ikura", + UnitPrice: 24.8000, + Quantity: 16, + Discontinued: false, + ExtendedPrice: 396.8000, + Freight: 47.4200 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Toronto Blvd.", + ShipCity: "Toronto", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Toronto Blvd.", + City: "Toronto", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Margaret Peacock", + OrderID: 10389, + OrderDate: new Date("10/4/2016"), + ShipperName: "United Package", + ProductID: 55, + ProductName: "P\u00e2t\u00e9 chinois", + UnitPrice: 19.2000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 288.0000, + Freight: 47.4200 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Margaret Peacock", + OrderID: 10389, + OrderDate: new Date("9/4/2018"), + ShipperName: "United Package", + ProductID: 62, + ProductName: "Tarte au sucre", + UnitPrice: 39.4000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 788.0000, + Freight: 47.4200 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Margaret Peacock", + OrderID: 10389, + OrderDate: new Date("9/4/2018"), + ShipperName: "United Package", + ProductID: 70, + ProductName: "Outback Lager", + UnitPrice: 12.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 360.0000, + Freight: 47.4200 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Janet Leverling", + OrderID: 10410, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 33, + ProductName: "Geitost", + UnitPrice: 2.0000, + Quantity: 49, + Discontinued: false, + ExtendedPrice: 98.0000, + Freight: 2.4000 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Toronto Blvd.", + ShipCity: "Toronto", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Toronto Blvd.", + City: "Toronto", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Janet Leverling", + OrderID: 10410, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 59, + ProductName: "Raclette Courdavault", + UnitPrice: 44.0000, + Quantity: 16, + Discontinued: false, + ExtendedPrice: 704.0000, + Freight: 2.4000 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Janet Leverling", + OrderID: 10742, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 3, + ProductName: "Aniseed Syrup", + UnitPrice: 10.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 200.0000, + Freight: 243.7300 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Janet Leverling", + OrderID: 10742, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 60, + ProductName: "Camembert Pierrot", + UnitPrice: 34.0000, + Quantity: 50, + Discontinued: false, + ExtendedPrice: 1700.0000, + Freight: 243.7300 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Janet Leverling", + OrderID: 10742, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 72, + ProductName: "Mozzarella di Giovanni", + UnitPrice: 34.8000, + Quantity: 35, + Discontinued: false, + ExtendedPrice: 1218.0000, + Freight: 243.7300 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Toronto Blvd.", + ShipCity: "Toronto", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Toronto Blvd.", + City: "Toronto", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Michael Suyama", + OrderID: 10944, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 56, + ProductName: "Gnocchi di nonna Alice", + UnitPrice: 38.0000, + Quantity: 18, + Discontinued: false, + ExtendedPrice: 684.0000, + Freight: 52.9200 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Toronto Blvd.", + ShipCity: "Toronto", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Toronto Blvd.", + City: "Toronto", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Andrew Fuller", + OrderID: 10949, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 6, + ProductName: "Grandma's Boysenberry Spread", + UnitPrice: 25.0000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 300.0000, + Freight: 74.4400 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Andrew Fuller", + OrderID: 10949, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 10, + ProductName: "Ikura", + UnitPrice: 31.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 930.0000, + Freight: 74.4400 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Andrew Fuller", + OrderID: 10949, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 17, + ProductName: "Alice Mutton", + UnitPrice: 39.0000, + Quantity: 6, + Discontinued: false, + ExtendedPrice: 234.0000, + Freight: 74.4400 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Andrew Fuller", + OrderID: 10949, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 62, + ProductName: "Tarte au sucre", + UnitPrice: 49.3000, + Quantity: 60, + Discontinued: false, + ExtendedPrice: 2958.0000, + Freight: 74.4400 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Nancy Davolio", + OrderID: 10975, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 8, + ProductName: "Northwoods Cranberry Sauce", + UnitPrice: 40.0000, + Quantity: 16, + Discontinued: false, + ExtendedPrice: 640.0000, + Freight: 32.2700 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Nancy Davolio", + OrderID: 10975, + OrderDate: new Date("9/4/2018"), + ShipperName: "Federal Shipping", + ProductID: 75, + ProductName: "Rh\u00f6nbr\u00e4u Klosterbier", + UnitPrice: 7.7500, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 77.5000, + Freight: 32.2700 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Toronto Blvd.", + ShipCity: "Toronto", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Toronto Blvd.", + City: "Toronto", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Anne Dodsworth", + OrderID: 10411, + OrderDate: new Date("5/12/2017"), + ShipperName: "Federal Shipping", + ProductID: 41, + ProductName: "Jack's New England Clam Chowder", + UnitPrice: 7.7000, + Quantity: 25, + Discontinued: true, + ExtendedPrice: 154.0000, + Freight: 23.6500 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Anne Dodsworth", + OrderID: 10411, + OrderDate: new Date("5/12/2017"), + ShipperName: "Federal Shipping", + ProductID: 44, + ProductName: "Gula Malacca", + UnitPrice: 15.5000, + Quantity: 40, + Discontinued: true, + ExtendedPrice: 496.0000, + Freight: 23.6500 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Anne Dodsworth", + OrderID: 10411, + OrderDate: new Date("5/12/2017"), + ShipperName: "Federal Shipping", + ProductID: 59, + ProductName: "Raclette Courdavault", + UnitPrice: 44.0000, + Quantity: 9, + Discontinued: true, + ExtendedPrice: 316.8000, + Freight: 23.6500 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Margaret Peacock", + OrderID: 10431, + OrderDate: new Date("5/12/2017"), + ShipperName: "United Package", + ProductID: 17, + ProductName: "Alice Mutton", + UnitPrice: 31.2000, + Quantity: 50, + Discontinued: true, + ExtendedPrice: 1170.0000, + Freight: 44.1700 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Margaret Peacock", + OrderID: 10431, + OrderDate: new Date("5/12/2017"), + ShipperName: "United Package", + ProductID: 40, + ProductName: "Boston Crab Meat", + UnitPrice: 14.7000, + Quantity: 50, + Discontinued: true, + ExtendedPrice: 551.2500, + Freight: 44.1700 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Margaret Peacock", + OrderID: 10431, + OrderDate: new Date("5/12/2017"), + ShipperName: "United Package", + ProductID: 47, + ProductName: "Zaanse koeken", + UnitPrice: 7.6000, + Quantity: 30, + Discontinued: true, + ExtendedPrice: 171.0000, + Freight: 44.1700 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Janet Leverling", + OrderID: 10918, + OrderDate: new Date("5/12/2017"), + ShipperName: "Federal Shipping", + ProductID: 1, + ProductName: "Chai", + UnitPrice: 18.0000, + Quantity: 60, + Discontinued: true, + ExtendedPrice: 810.0000, + Freight: 48.8300 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Janet Leverling", + OrderID: 10918, + OrderDate: new Date("5/12/2017"), + ShipperName: "Federal Shipping", + ProductID: 60, + ProductName: "Camembert Pierrot", + UnitPrice: 34.0000, + Quantity: 25, + Discontinued: true, + ExtendedPrice: 637.5000, + Freight: 48.8300 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Michael Suyama", + OrderID: 10944, + OrderDate: new Date("5/12/2017"), + ShipperName: "Federal Shipping", + ProductID: 11, + ProductName: "Queso Cabrales", + UnitPrice: 21.0000, + Quantity: 5, + Discontinued: true, + ExtendedPrice: 78.7500, + Freight: 52.9200 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Michael Suyama", + OrderID: 10944, + OrderDate: new Date("5/12/2017"), + ShipperName: "Federal Shipping", + ProductID: 44, + ProductName: "Gula Malacca", + UnitPrice: 19.4500, + Quantity: 18, + Discontinued: true, + ExtendedPrice: 262.5800, + Freight: 52.9200 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Nancy Davolio", + OrderID: 11027, + OrderDate: new Date("5/12/2017"), + ShipperName: "Speedy Express", + ProductID: 24, + ProductName: "Guaran\u00e1 Fant\u00e1stica", + UnitPrice: 4.5000, + Quantity: 30, + Discontinued: true, + ExtendedPrice: 101.2500, + Freight: 52.5200 +}, { + ShipName: "Bottom-Dollar Markets", + ShipAddress: "23 Tsawassen Blvd.", + ShipCity: "Tsawassen", + ShipRegion: "BC", + ShipPostalCode: "T2F 8M4", + ShipCountry: "Canada", + CustomerID: "BOTTM", + CustomerName: "Bottom-Dollar Markets", + Address: "23 Tsawassen Blvd.", + City: "Tsawassen", + Region: "BC", + PostalCode: "T2F 8M4", + Country: "Canada", + Salesperson: "Nancy Davolio", + OrderID: 11027, + OrderDate: new Date("5/12/2017"), + ShipperName: "Speedy Express", + ProductID: 62, + ProductName: "Tarte au sucre", + UnitPrice: 49.3000, + Quantity: 21, + Discontinued: true, + ExtendedPrice: 776.4800, + Freight: 52.5200 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Robert King", + OrderID: 10289, + OrderDate: new Date("5/12/2017"), + ShipperName: "Federal Shipping", + ProductID: 3, + ProductName: "Aniseed Syrup", + UnitPrice: 8.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 240.0000, + Freight: 22.7700 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Robert King", + OrderID: 10289, + OrderDate: new Date("5/12/2017"), + ShipperName: "Federal Shipping", + ProductID: 64, + ProductName: "Wimmers gute Semmelkn\u00f6del", + UnitPrice: 26.6000, + Quantity: 9, + Discontinued: false, + ExtendedPrice: 239.4000, + Freight: 22.7700 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Andrew Fuller", + OrderID: 10471, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 7, + ProductName: "Uncle Bob's Organic Dried Pears", + UnitPrice: 24.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 720.0000, + Freight: 45.5900 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Andrew Fuller", + OrderID: 10471, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 56, + ProductName: "Gnocchi di nonna Alice", + UnitPrice: 30.4000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 608.0000, + Freight: 45.5900 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Janet Leverling", + OrderID: 10484, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 21, + ProductName: "Sir Rodney's Scones", + UnitPrice: 8.0000, + Quantity: 14, + Discontinued: false, + ExtendedPrice: 112.0000, + Freight: 6.8800 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Janet Leverling", + OrderID: 10484, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 40, + ProductName: "Boston Crab Meat", + UnitPrice: 14.7000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 147.0000, + Freight: 6.8800 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Janet Leverling", + OrderID: 10484, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 51, + ProductName: "Manjimup Dried Apples", + UnitPrice: 42.4000, + Quantity: 3, + Discontinued: false, + ExtendedPrice: 127.2000, + Freight: 6.8800 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Anne Dodsworth", + OrderID: 10538, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 70, + ProductName: "Outback Lager", + UnitPrice: 15.0000, + Quantity: 7, + Discontinued: false, + ExtendedPrice: 105.0000, + Freight: 4.8700 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Anne Dodsworth", + OrderID: 10538, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 72, + ProductName: "Mozzarella di Giovanni", + UnitPrice: 34.8000, + Quantity: 1, + Discontinued: false, + ExtendedPrice: 34.8000, + Freight: 4.8700 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Michael Suyama", + OrderID: 10539, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 13, + ProductName: "Konbu", + UnitPrice: 6.0000, + Quantity: 8, + Discontinued: false, + ExtendedPrice: 48.0000, + Freight: 12.3600 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Michael Suyama", + OrderID: 10539, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 21, + ProductName: "Sir Rodney's Scones", + UnitPrice: 10.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 150.0000, + Freight: 12.3600 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Michael Suyama", + OrderID: 10539, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 33, + ProductName: "Geitost", + UnitPrice: 2.5000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 37.5000, + Freight: 12.3600 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Michael Suyama", + OrderID: 10539, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 49, + ProductName: "Maxilaku", + UnitPrice: 20.0000, + Quantity: 6, + Discontinued: false, + ExtendedPrice: 120.0000, + Freight: 12.3600 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Janet Leverling", + OrderID: 10947, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 59, + ProductName: "Raclette Courdavault", + UnitPrice: 55.0000, + Quantity: 4, + Discontinued: false, + ExtendedPrice: 220.0000, + Freight: 3.2600 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 11023, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 7, + ProductName: "Uncle Bob's Organic Dried Pears", + UnitPrice: 30.0000, + Quantity: 4, + Discontinued: false, + ExtendedPrice: 120.0000, + Freight: 123.8300 +}, { + ShipName: "B's Beverages", + ShipAddress: "Fauntleroy Circus", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "EC2 5NT", + ShipCountry: "UK", + CustomerID: "BSBEV", + CustomerName: "B's Beverages", + Address: "Fauntleroy Circus", + City: "London", + Region: null, + PostalCode: "EC2 5NT", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 11023, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 43, + ProductName: "Ipoh Coffee", + UnitPrice: 46.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 1380.0000, + Freight: 123.8300 +}, { + ShipName: "Cactus Comidas para llevar", + ShipAddress: "Cerrito 333", + ShipCity: "Buenos Aires", + ShipRegion: null, + ShipPostalCode: "1010", + ShipCountry: "Argentina", + CustomerID: "CACTU", + CustomerName: "Cactus Comidas para llevar", + Address: "Cerrito 333", + City: "Buenos Aires", + Region: null, + PostalCode: "1010", + Country: "Argentina", + Salesperson: "Laura Callahan", + OrderID: 10521, + OrderDate: new Date("4/14/2017"), + ShipperName: "United Package", + ProductID: 68, + ProductName: "Scottish Longbreads", + UnitPrice: 12.5000, + Quantity: 6, + Discontinued: true, + ExtendedPrice: 75.0000, + Freight: 17.2200 +}, { + ShipName: "Cactus Comidas para llevar", + ShipAddress: "Cerrito 333", + ShipCity: "Buenos Aires", + ShipRegion: null, + ShipPostalCode: "1010", + ShipCountry: "Argentina", + CustomerID: "CACTU", + CustomerName: "Cactus Comidas para llevar", + Address: "Cerrito 333", + City: "Buenos Aires", + Region: null, + PostalCode: "1010", + Country: "Argentina", + Salesperson: "Laura Callahan", + OrderID: 11054, + OrderDate: new Date("4/14/2017"), + ShipperName: "Speedy Express", + ProductID: 33, + ProductName: "Geitost", + UnitPrice: 2.5000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 25.0000, + Freight: 0.3300 +}, { + ShipName: "Cactus Comidas para llevar", + ShipAddress: "Cerrito 333", + ShipCity: "Buenos Aires", + ShipRegion: null, + ShipPostalCode: "1010", + ShipCountry: "Argentina", + CustomerID: "CACTU", + CustomerName: "Cactus Comidas para llevar", + Address: "Cerrito 333", + City: "Buenos Aires", + Region: null, + PostalCode: "1010", + Country: "Argentina", + Salesperson: "Laura Callahan", + OrderID: 11054, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 67, + ProductName: "Laughing Lumberjack Lager", + UnitPrice: 14.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 280.0000, + Freight: 0.3300 +}, { + ShipName: "Centro comercial Moctezuma", + ShipAddress: "Sierras de Granada 9993", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05022", + ShipCountry: "Mexico", + CustomerID: "CENTC", + CustomerName: "Centro comercial Moctezuma", + Address: "Sierras de Granada 9993", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05022", + Country: "Mexico", + Salesperson: "Margaret Peacock", + OrderID: 10259, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 21, + ProductName: "Sir Rodney's Scones", + UnitPrice: 8.0000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 80.0000, + Freight: 3.2500 +}, { + ShipName: "Centro comercial Moctezuma", + ShipAddress: "Sierras de Granada 9993", + ShipCity: "M\u00e9xico D.F.", + ShipRegion: null, + ShipPostalCode: "05022", + ShipCountry: "Mexico", + CustomerID: "CENTC", + CustomerName: "Centro comercial Moctezuma", + Address: "Sierras de Granada 9993", + City: "M\u00e9xico D.F.", + Region: null, + PostalCode: "05022", + Country: "Mexico", + Salesperson: "Margaret Peacock", + OrderID: 10259, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 37, + ProductName: "Gravad lax", + UnitPrice: 20.8000, + Quantity: 1, + Discontinued: false, + ExtendedPrice: 20.8000, + Freight: 3.2500 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Steven Buchanan", + OrderID: 10254, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 74, + ProductName: "Longlife Tofu", + UnitPrice: 8.0000, + Quantity: 21, + Discontinued: false, + ExtendedPrice: 168.0000, + Freight: 22.9800 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Michael Suyama", + OrderID: 10370, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 64, + ProductName: "Wimmers gute Semmelkn\u00f6del", + UnitPrice: 26.6000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 798.0000, + Freight: 1.1700 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Michael Suyama", + OrderID: 10519, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 56, + ProductName: "Gnocchi di nonna Alice", + UnitPrice: 38.0000, + Quantity: 40, + Discontinued: false, + ExtendedPrice: 1520.0000, + Freight: 91.7600 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Nancy Davolio", + OrderID: 10746, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 13, + ProductName: "Konbu", + UnitPrice: 6.0000, + Quantity: 6, + Discontinued: false, + ExtendedPrice: 36.0000, + Freight: 31.4300 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Nancy Davolio", + OrderID: 10746, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 42, + ProductName: "Singaporean Hokkien Fried Mee", + UnitPrice: 14.0000, + Quantity: 28, + Discontinued: false, + ExtendedPrice: 392.0000, + Freight: 31.4300 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Nancy Davolio", + OrderID: 10746, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 62, + ProductName: "Tarte au sucre", + UnitPrice: 49.3000, + Quantity: 9, + Discontinued: false, + ExtendedPrice: 443.7000, + Freight: 31.4300 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Nancy Davolio", + OrderID: 10746, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 69, + ProductName: "Gudbrandsdalsost", + UnitPrice: 36.0000, + Quantity: 40, + Discontinued: false, + ExtendedPrice: 1440.0000, + Freight: 31.4300 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Margaret Peacock", + OrderID: 10966, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 37, + ProductName: "Gravad lax", + UnitPrice: 26.0000, + Quantity: 8, + Discontinued: false, + ExtendedPrice: 208.0000, + Freight: 27.1900 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Margaret Peacock", + OrderID: 11029, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 56, + ProductName: "Gnocchi di nonna Alice", + UnitPrice: 38.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 760.0000, + Freight: 47.8400 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Margaret Peacock", + OrderID: 11029, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 63, + ProductName: "Vegie-spread", + UnitPrice: 43.9000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 526.8000, + Freight: 47.8400 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Janet Leverling", + OrderID: 11041, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 63, + ProductName: "Vegie-spread", + UnitPrice: 43.9000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 1317.0000, + Freight: 48.2200 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Michael Suyama", + OrderID: 10519, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 10, + ProductName: "Ikura", + UnitPrice: 31.0000, + Quantity: 16, + Discontinued: true, + ExtendedPrice: 471.2000, + Freight: 91.7600 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Michael Suyama", + OrderID: 10519, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 60, + ProductName: "Camembert Pierrot", + UnitPrice: 34.0000, + Quantity: 10, + Discontinued: true, + ExtendedPrice: 323.0000, + Freight: 91.7600 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Robert King", + OrderID: 10731, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 21, + ProductName: "Sir Rodney's Scones", + UnitPrice: 10.0000, + Quantity: 40, + Discontinued: true, + ExtendedPrice: 380.0000, + Freight: 96.6500 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Robert King", + OrderID: 10731, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 51, + ProductName: "Manjimup Dried Apples", + UnitPrice: 53.0000, + Quantity: 30, + Discontinued: true, + ExtendedPrice: 1510.5000, + Freight: 96.6500 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Steven Buchanan", + OrderID: 10254, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 24, + ProductName: "Guaran\u00e1 Fant\u00e1stica", + UnitPrice: 3.6000, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 45.9000, + Freight: 22.9800 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Steven Buchanan", + OrderID: 10254, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 55, + ProductName: "P\u00e2t\u00e9 chinois", + UnitPrice: 19.2000, + Quantity: 21, + Discontinued: true, + ExtendedPrice: 342.7200, + Freight: 22.9800 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Michael Suyama", + OrderID: 10370, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 1, + ProductName: "Chai", + UnitPrice: 14.4000, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 183.6000, + Freight: 1.1700 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Michael Suyama", + OrderID: 10370, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 74, + ProductName: "Longlife Tofu", + UnitPrice: 8.0000, + Quantity: 20, + Discontinued: true, + ExtendedPrice: 136.0000, + Freight: 1.1700 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Margaret Peacock", + OrderID: 10966, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 56, + ProductName: "Gnocchi di nonna Alice", + UnitPrice: 38.0000, + Quantity: 12, + Discontinued: true, + ExtendedPrice: 387.6000, + Freight: 27.1900 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Margaret Peacock", + OrderID: 10966, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 62, + ProductName: "Tarte au sucre", + UnitPrice: 49.3000, + Quantity: 12, + Discontinued: true, + ExtendedPrice: 502.8600, + Freight: 27.1900 +}, { + ShipName: "Chop-suey Chinese", + ShipAddress: "Hauptstr. 31", + ShipCity: "Bern", + ShipRegion: null, + ShipPostalCode: "3012", + ShipCountry: "Switzerland", + CustomerID: "CHOPS", + CustomerName: "Chop-suey Chinese", + Address: "Hauptstr. 29", + City: "Bern", + Region: null, + PostalCode: "3012", + Country: "Switzerland", + Salesperson: "Janet Leverling", + OrderID: 11041, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 2, + ProductName: "Chang", + UnitPrice: 19.0000, + Quantity: 30, + Discontinued: true, + ExtendedPrice: 456.0000, + Freight: 48.2200 +}, { + ShipName: "Com\u00e9rcio Mineiro", + ShipAddress: "Av. dos Lus\u00edadas, 23", + ShipCity: "Rio de Janeiro", + ShipRegion: "SP", + ShipPostalCode: "05432-043", + ShipCountry: "Brazil", + CustomerID: "COMMI", + CustomerName: "Com\u00e9rcio Mineiro", + Address: "Av. dos Lus\u00edadas, 23", + City: "Rio de Janeiro", + Region: "SP", + PostalCode: "05432-043", + Country: "Brazil", + Salesperson: "Laura Callahan", + OrderID: 10290, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 5, + ProductName: "Chef Anton's Gumbo Mix", + UnitPrice: 17.0000, + Quantity: 20, + Discontinued: true, + ExtendedPrice: 340.0000, + Freight: 79.7000 +}, { + ShipName: "Com\u00e9rcio Mineiro", + ShipAddress: "Av. dos Lus\u00edadas, 23", + ShipCity: "Sao Paulo", + ShipRegion: "SP", + ShipPostalCode: "05432-043", + ShipCountry: "Brazil", + CustomerID: "COMMI", + CustomerName: "Com\u00e9rcio Mineiro", + Address: "Av. dos Lus\u00edadas, 23", + City: "Sao Paulo", + Region: "SP", + PostalCode: "05432-043", + Country: "Brazil", + Salesperson: "Laura Callahan", + OrderID: 10290, + OrderDate: new Date("2/15/2017"), + ShipperName: "Speedy Express", + ProductID: 29, + ProductName: "Th\u00fcringer Rostbratwurst", + UnitPrice: 99.0000, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 1485.0000, + Freight: 79.7000 +}, { + ShipName: "Com\u00e9rcio Mineiro", + ShipAddress: "Av. dos Lus\u00edadas, 23", + ShipCity: "Rio de Janeiro", + ShipRegion: "SP", + ShipPostalCode: "05432-043", + ShipCountry: "Brazil", + CustomerID: "COMMI", + CustomerName: "Com\u00e9rcio Mineiro", + Address: "Av. dos Lus\u00edadas, 23", + City: "Rio de Janeiro", + Region: "SP", + PostalCode: "05432-043", + Country: "Brazil", + Salesperson: "Laura Callahan", + OrderID: 10290, + OrderDate: new Date("4/14/2017"), + ShipperName: "Speedy Express", + ProductID: 49, + ProductName: "Maxilaku", + UnitPrice: 16.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 240.0000, + Freight: 79.7000 +}, { + ShipName: "Com\u00e9rcio Mineiro", + ShipAddress: "Av. dos Lus\u00edadas, 23", + ShipCity: "Sao Paulo", + ShipRegion: "SP", + ShipPostalCode: "05432-043", + ShipCountry: "Brazil", + CustomerID: "COMMI", + CustomerName: "Com\u00e9rcio Mineiro", + Address: "Av. dos Lus\u00edadas, 23", + City: "Sao Paulo", + Region: "SP", + PostalCode: "05432-043", + Country: "Brazil", + Salesperson: "Laura Callahan", + OrderID: 10290, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 77, + ProductName: "Original Frankfurter gr\u00fcne So\u00dfe", + UnitPrice: 10.4000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 104.0000, + Freight: 79.7000 +}, { + ShipName: "Com\u00e9rcio Mineiro", + ShipAddress: "Av. dos Lus\u00edadas, 23", + ShipCity: "Sao Paulo", + ShipRegion: "SP", + ShipPostalCode: "05432-043", + ShipCountry: "Brazil", + CustomerID: "COMMI", + CustomerName: "Com\u00e9rcio Mineiro", + Address: "Av. dos Lus\u00edadas, 23", + City: "Sao Paulo", + Region: "SP", + PostalCode: "05432-043", + Country: "Brazil", + Salesperson: "Margaret Peacock", + OrderID: 10466, + OrderDate: new Date("5/4/2017"), + ShipperName: "Speedy Express", + ProductID: 11, + ProductName: "Queso Cabrales", + UnitPrice: 16.8000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 168.0000, + Freight: 11.9300 +}, { + ShipName: "Com\u00e9rcio Mineiro", + ShipAddress: "Av. dos Lus\u00edadas, 23", + ShipCity: "Sao Paulo", + ShipRegion: "SP", + ShipPostalCode: "05432-043", + ShipCountry: "Brazil", + CustomerID: "COMMI", + CustomerName: "Com\u00e9rcio Mineiro", + Address: "Av. dos Lus\u00edadas, 23", + City: "Sao Paulo", + Region: "SP", + PostalCode: "05432-043", + Country: "Brazil", + Salesperson: "Margaret Peacock", + OrderID: 10466, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 46, + ProductName: "Spegesild", + UnitPrice: 9.6000, + Quantity: 5, + Discontinued: false, + ExtendedPrice: 48.0000, + Freight: 11.9300 +}, { + ShipName: "Com\u00e9rcio Mineiro", + ShipAddress: "Av. dos Lus\u00edadas, 23", + ShipCity: "Rio de Janeiro", + ShipRegion: "SP", + ShipPostalCode: "05432-043", + ShipCountry: "Brazil", + CustomerID: "COMMI", + CustomerName: "Com\u00e9rcio Mineiro", + Address: "Av. dos Lus\u00edadas, 23", + City: "Rio de Janeiro", + Region: "SP", + PostalCode: "05432-043", + Country: "Brazil", + Salesperson: "Margaret Peacock", + OrderID: 10494, + OrderDate: new Date("4/1/2017"), + ShipperName: "United Package", + ProductID: 56, + ProductName: "Gnocchi di nonna Alice", + UnitPrice: 30.4000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 912.0000, + Freight: 65.9900 +}, { + ShipName: "Com\u00e9rcio Mineiro", + ShipAddress: "Av. dos Lus\u00edadas, 23", + ShipCity: "Rio de Janeiro", + ShipRegion: "SP", + ShipPostalCode: "05432-043", + ShipCountry: "Brazil", + CustomerID: "COMMI", + CustomerName: "Com\u00e9rcio Mineiro", + Address: "Av. dos Lus\u00edadas, 23", + City: "Rio de Janeiro", + Region: "SP", + PostalCode: "05432-043", + Country: "Brazil", + Salesperson: "Nancy Davolio", + OrderID: 10969, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 46, + ProductName: "Spegesild", + UnitPrice: 12.0000, + Quantity: 9, + Discontinued: false, + ExtendedPrice: 108.0000, + Freight: 0.2100 +}, { + ShipName: "Com\u00e9rcio Mineiro", + ShipAddress: "Av. dos Lus\u00edadas, 23", + ShipCity: "Sao Paulo", + ShipRegion: "SP", + ShipPostalCode: "05432-043", + ShipCountry: "Brazil", + CustomerID: "COMMI", + CustomerName: "Com\u00e9rcio Mineiro", + Address: "Av. dos Lus\u00edadas, 23", + City: "Sao Paulo", + Region: "SP", + PostalCode: "05432-043", + Country: "Brazil", + Salesperson: "Andrew Fuller", + OrderID: 11042, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 44, + ProductName: "Gula Malacca", + UnitPrice: 19.4500, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 291.7500, + Freight: 29.9900 +}, { + ShipName: "Com\u00e9rcio Mineiro", + ShipAddress: "Av. dos Lus\u00edadas, 23", + ShipCity: "Sao Paulo", + ShipRegion: "SP", + ShipPostalCode: "05432-043", + ShipCountry: "Brazil", + CustomerID: "COMMI", + CustomerName: "Com\u00e9rcio Mineiro", + Address: "Av. dos Lus\u00edadas, 23", + City: "Sao Paulo", + Region: "SP", + PostalCode: "05432-043", + Country: "Brazil", + Salesperson: "Andrew Fuller", + OrderID: 11042, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 61, + ProductName: "Sirop d'\u00e9rable", + UnitPrice: 28.5000, + Quantity: 4, + Discontinued: false, + ExtendedPrice: 114.0000, + Freight: 29.9900 +}, { + ShipName: "Consolidated Holdings", + ShipAddress: "Berkeley Gardens 12 Brewery", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX1 6LT", + ShipCountry: "UK", + CustomerID: "CONSH", + CustomerName: "Consolidated Holdings", + Address: "Berkeley Gardens 12 Brewery", + City: "London", + Region: null, + PostalCode: "WX1 6LT", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 10435, + OrderDate: new Date("4/16/2016"), + ShipperName: "United Package", + ProductID: 2, + ProductName: "Chang", + UnitPrice: 15.2000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 152.0000, + Freight: 9.2100 +}, { + ShipName: "Consolidated Holdings", + ShipAddress: "Berkeley Gardens 12 Brewery", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX1 6LT", + ShipCountry: "UK", + CustomerID: "CONSH", + CustomerName: "Consolidated Holdings", + Address: "Berkeley Gardens 12 Brewery", + City: "London", + Region: null, + PostalCode: "WX1 6LT", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 10435, + OrderDate: new Date("5/12/2016"), + ShipperName: "United Package", + ProductID: 22, + ProductName: "Gustaf's Kn\u00e4ckebr\u00f6d", + UnitPrice: 16.8000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 201.6000, + Freight: 9.2100 +}, { + ShipName: "Consolidated Holdings", + ShipAddress: "Berkeley Gardens 12 Brewery", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX1 6LT", + ShipCountry: "UK", + CustomerID: "CONSH", + CustomerName: "Consolidated Holdings", + Address: "Berkeley Gardens 12 Brewery", + City: "London", + Region: null, + PostalCode: "WX1 6LT", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 10435, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 72, + ProductName: "Mozzarella di Giovanni", + UnitPrice: 27.8000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 278.0000, + Freight: 9.2100 +}, { + ShipName: "Consolidated Holdings", + ShipAddress: "Berkeley Gardens 12 Brewery", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX1 6LT", + ShipCountry: "UK", + CustomerID: "CONSH", + CustomerName: "Consolidated Holdings", + Address: "Berkeley Gardens 12 Brewery", + City: "London", + Region: null, + PostalCode: "WX1 6LT", + Country: "UK", + Salesperson: "Andrew Fuller", + OrderID: 10462, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 13, + ProductName: "Konbu", + UnitPrice: 4.8000, + Quantity: 1, + Discontinued: false, + ExtendedPrice: 4.8000, + Freight: 6.1700 +}, { + ShipName: "Consolidated Holdings", + ShipAddress: "Berkeley Gardens 12 Brewery", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX1 6LT", + ShipCountry: "UK", + CustomerID: "CONSH", + CustomerName: "Consolidated Holdings", + Address: "Berkeley Gardens 12 Brewery", + City: "London", + Region: null, + PostalCode: "WX1 6LT", + Country: "UK", + Salesperson: "Andrew Fuller", + OrderID: 10462, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 23, + ProductName: "Tunnbr\u00f6d", + UnitPrice: 7.2000, + Quantity: 21, + Discontinued: false, + ExtendedPrice: 151.2000, + Freight: 6.1700 +}, { + ShipName: "Consolidated Holdings", + ShipAddress: "Berkeley Gardens 12 Brewery", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX1 6LT", + ShipCountry: "UK", + CustomerID: "CONSH", + CustomerName: "Consolidated Holdings", + Address: "Berkeley Gardens 12 Brewery", + City: "London", + Region: null, + PostalCode: "WX1 6LT", + Country: "UK", + Salesperson: "Robert King", + OrderID: 10848, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 5, + ProductName: "Chef Anton's Gumbo Mix", + UnitPrice: 21.3500, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 640.5000, + Freight: 38.2400 +}, { + ShipName: "Consolidated Holdings", + ShipAddress: "Berkeley Gardens 12 Brewery", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX1 6LT", + ShipCountry: "UK", + CustomerID: "CONSH", + CustomerName: "Consolidated Holdings", + Address: "Berkeley Gardens 12 Brewery", + City: "London", + Region: null, + PostalCode: "WX1 6LT", + Country: "UK", + Salesperson: "Robert King", + OrderID: 10848, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 9, + ProductName: "Mishi Kobe Niku", + UnitPrice: 97.0000, + Quantity: 3, + Discontinued: false, + ExtendedPrice: 291.0000, + Freight: 38.2400 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 10301, + OrderDate: new Date("4/14/2017"), + ShipperName: "United Package", + ProductID: 40, + ProductName: "Boston Crab Meat", + UnitPrice: 14.7000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 147.0000, + Freight: 45.0800 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 10301, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 56, + ProductName: "Gnocchi di nonna Alice", + UnitPrice: 30.4000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 608.0000, + Freight: 45.0800 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Andrew Fuller", + OrderID: 10312, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 28, + ProductName: "R\u00f6ssle Sauerkraut", + UnitPrice: 36.4000, + Quantity: 4, + Discontinued: false, + ExtendedPrice: 145.6000, + Freight: 40.2600 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Andrew Fuller", + OrderID: 10312, + OrderDate: new Date("4/14/2018"), + ShipperName: "United Package", + ProductID: 43, + ProductName: "Ipoh Coffee", + UnitPrice: 36.8000, + Quantity: 24, + Discontinued: false, + ExtendedPrice: 883.2000, + Freight: 40.2600 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Andrew Fuller", + OrderID: 10312, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 53, + ProductName: "Perth Pasties", + UnitPrice: 26.2000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 524.0000, + Freight: 40.2600 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Andrew Fuller", + OrderID: 10312, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 75, + ProductName: "Rh\u00f6nbr\u00e4u Klosterbier", + UnitPrice: 6.2000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 62.0000, + Freight: 40.2600 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10348, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 23, + ProductName: "Tunnbr\u00f6d", + UnitPrice: 7.2000, + Quantity: 25, + Discontinued: false, + ExtendedPrice: 180.0000, + Freight: 0.7800 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Michael Suyama", + OrderID: 10356, + OrderDate: new Date("4/14/2018"), + ShipperName: "United Package", + ProductID: 31, + ProductName: "Gorgonzola Telino", + UnitPrice: 10.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 300.0000, + Freight: 36.7100 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Michael Suyama", + OrderID: 10356, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 55, + ProductName: "P\u00e2t\u00e9 chinois", + UnitPrice: 19.2000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 230.4000, + Freight: 36.7100 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Michael Suyama", + OrderID: 10356, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 69, + ProductName: "Gudbrandsdalsost", + UnitPrice: 28.8000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 576.0000, + Freight: 36.7100 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 10632, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 2, + ProductName: "Chang", + UnitPrice: 19.0000, + Quantity: 30, + Discontinued: true, + ExtendedPrice: 541.5000, + Freight: 41.3800 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 10632, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 33, + ProductName: "Geitost", + UnitPrice: 2.5000, + Quantity: 20, + Discontinued: true, + ExtendedPrice: 47.5000, + Freight: 41.3800 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 11046, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 12, + ProductName: "Queso Manchego La Pastora", + UnitPrice: 38.0000, + Quantity: 20, + Discontinued: true, + ExtendedPrice: 722.0000, + Freight: 71.6400 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 11046, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 32, + ProductName: "Mascarpone Fabioli", + UnitPrice: 32.0000, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 456.0000, + Freight: 71.6400 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 11046, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 35, + ProductName: "Steeleye Stout", + UnitPrice: 18.0000, + Quantity: 18, + Discontinued: true, + ExtendedPrice: 307.8000, + Freight: 71.6400 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Nancy Davolio", + OrderID: 10668, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 31, + ProductName: "Gorgonzola Telino", + UnitPrice: 12.5000, + Quantity: 8, + Discontinued: true, + ExtendedPrice: 90.0000, + Freight: 47.2200 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Nancy Davolio", + OrderID: 10668, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 55, + ProductName: "P\u00e2t\u00e9 chinois", + UnitPrice: 24.0000, + Quantity: 4, + Discontinued: true, + ExtendedPrice: 86.4000, + Freight: 47.2200 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Nancy Davolio", + OrderID: 10668, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 64, + ProductName: "Wimmers gute Semmelkn\u00f6del", + UnitPrice: 33.2500, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 448.8700, + Freight: 47.2200 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10348, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 1, + ProductName: "Chai", + UnitPrice: 14.4000, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 183.6000, + Freight: 0.7800 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Robert King", + OrderID: 10513, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 21, + ProductName: "Sir Rodney's Scones", + UnitPrice: 10.0000, + Quantity: 40, + Discontinued: true, + ExtendedPrice: 320.0000, + Freight: 105.6500 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Robert King", + OrderID: 10513, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 32, + ProductName: "Mascarpone Fabioli", + UnitPrice: 32.0000, + Quantity: 50, + Discontinued: true, + ExtendedPrice: 1280.0000, + Freight: 105.6500 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Robert King", + OrderID: 10513, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 61, + ProductName: "Sirop d'\u00e9rable", + UnitPrice: 28.5000, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 342.0000, + Freight: 105.6500 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10640, + OrderDate: new Date("4/14/2017"), + ShipperName: "Speedy Express", + ProductID: 69, + ProductName: "Gudbrandsdalsost", + UnitPrice: 36.0000, + Quantity: 20, + Discontinued: true, + ExtendedPrice: 540.0000, + Freight: 23.5500 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10640, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 70, + ProductName: "Outback Lager", + UnitPrice: 15.0000, + Quantity: 15, + Discontinued: true, + ExtendedPrice: 168.7500, + Freight: 23.5500 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 10651, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 19, + ProductName: "Teatime Chocolate Biscuits", + UnitPrice: 9.2000, + Quantity: 12, + Discontinued: true, + ExtendedPrice: 82.8000, + Freight: 20.6000 +}, { + ShipName: "Die Wandernde Kuh", + ShipAddress: "Adenauerallee 900", + ShipCity: "Stuttgart", + ShipRegion: null, + ShipPostalCode: "70563", + ShipCountry: "Germany", + CustomerID: "WANDK", + CustomerName: "Die Wandernde Kuh", + Address: "Adenauerallee 900", + City: "Stuttgart", + Region: null, + PostalCode: "70563", + Country: "Germany", + Salesperson: "Laura Callahan", + OrderID: 10651, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 22, + ProductName: "Gustaf's Kn\u00e4ckebr\u00f6d", + UnitPrice: 21.0000, + Quantity: 20, + Discontinued: true, + ExtendedPrice: 315.0000, + Freight: 20.6000 +}, { + ShipName: "Drachenblut Delikatessen", + ShipAddress: "Walserweg 21", + ShipCity: "Aachen", + ShipRegion: null, + ShipPostalCode: "52066", + ShipCountry: "Germany", + CustomerID: "DRACD", + CustomerName: "Drachenblut Delikatessen", + Address: "Walserweg 21", + City: "Aachen", + Region: null, + PostalCode: "52066", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10363, + OrderDate: new Date("4/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 31, + ProductName: "Gorgonzola Telino", + UnitPrice: 10.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 200.0000, + Freight: 30.5400 +}, { + ShipName: "Drachenblut Delikatessen", + ShipAddress: "Walserweg 21", + ShipCity: "Aachen", + ShipRegion: null, + ShipPostalCode: "52066", + ShipCountry: "Germany", + CustomerID: "DRACD", + CustomerName: "Drachenblut Delikatessen", + Address: "Walserweg 21", + City: "Aachen", + Region: null, + PostalCode: "52066", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10363, + OrderDate: new Date("12/4/2016"), + ShipperName: "Federal Shipping", + ProductID: 75, + ProductName: "Rh\u00f6nbr\u00e4u Klosterbier", + UnitPrice: 6.2000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 74.4000, + Freight: 30.5400 +}, { + ShipName: "Drachenblut Delikatessen", + ShipAddress: "Walserweg 21", + ShipCity: "Aachen", + ShipRegion: null, + ShipPostalCode: "52066", + ShipCountry: "Germany", + CustomerID: "DRACD", + CustomerName: "Drachenblut Delikatessen", + Address: "Walserweg 21", + City: "Aachen", + Region: null, + PostalCode: "52066", + Country: "Germany", + Salesperson: "Margaret Peacock", + OrderID: 10363, + OrderDate: new Date("2/8/2016"), + ShipperName: "Federal Shipping", + ProductID: 76, + ProductName: "Lakkalik\u00f6\u00f6ri", + UnitPrice: 14.4000, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 172.8000, + Freight: 30.5400 +}, { + ShipName: "Drachenblut Delikatessen", + ShipAddress: "Walserweg 21", + ShipCity: "Aachen", + ShipRegion: null, + ShipPostalCode: "52066", + ShipCountry: "Germany", + CustomerID: "DRACD", + CustomerName: "Drachenblut Delikatessen", + Address: "Walserweg 21", + City: "Aachen", + Region: null, + PostalCode: "52066", + Country: "Germany", + Salesperson: "Janet Leverling", + OrderID: 10391, + OrderDate: new Date("4/14/2017"), + ShipperName: "Federal Shipping", + ProductID: 13, + ProductName: "Konbu", + UnitPrice: 4.8000, + Quantity: 18, + Discontinued: false, + ExtendedPrice: 86.4000, + Freight: 5.4500 +}, { + ShipName: "Drachenblut Delikatessen", + ShipAddress: "Walserweg 21", + ShipCity: "Aachen", + ShipRegion: null, + ShipPostalCode: "52066", + ShipCountry: "Germany", + CustomerID: "DRACD", + CustomerName: "Drachenblut Delikatessen", + Address: "Walserweg 21", + City: "Aachen", + Region: null, + PostalCode: "52066", + Country: "Germany", + Salesperson: "Robert King", + OrderID: 10797, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 11, + ProductName: "Queso Cabrales", + UnitPrice: 21.0000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 420.0000, + Freight: 33.3500 +}, { + ShipName: "Drachenblut Delikatessen", + ShipAddress: "Walserweg 21", + ShipCity: "Aachen", + ShipRegion: null, + ShipPostalCode: "52066", + ShipCountry: "Germany", + CustomerID: "DRACD", + CustomerName: "Drachenblut Delikatessen", + Address: "Walserweg 21", + City: "Aachen", + Region: null, + PostalCode: "52066", + Country: "Germany", + Salesperson: "Nancy Davolio", + OrderID: 11067, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 41, + ProductName: "Jack's New England Clam Chowder", + UnitPrice: 9.6500, + Quantity: 9, + Discontinued: false, + ExtendedPrice: 86.8500, + Freight: 7.9800 +}, { + ShipName: "Du monde entier", + ShipAddress: "67, rue des Cinquante Otages", + ShipCity: "Nantes", + ShipRegion: null, + ShipPostalCode: "44000", + ShipCountry: "France", + CustomerID: "DUMON", + CustomerName: "Du monde entier", + Address: "67, rue des Cinquante Otages", + City: "Nantes", + Region: null, + PostalCode: "44000", + Country: "France", + Salesperson: "Nancy Davolio", + OrderID: 10311, + OrderDate: new Date("8/14/2016"), + ShipperName: "Federal Shipping", + ProductID: 42, + ProductName: "Singaporean Hokkien Fried Mee", + UnitPrice: 11.2000, + Quantity: 6, + Discontinued: false, + ExtendedPrice: 67.2000, + Freight: 24.6900 +}, { + ShipName: "Du monde entier", + ShipAddress: "67, rue des Cinquante Otages", + ShipCity: "Nantes", + ShipRegion: null, + ShipPostalCode: "44000", + ShipCountry: "France", + CustomerID: "DUMON", + CustomerName: "Du monde entier", + Address: "67, rue des Cinquante Otages", + City: "Nantes", + Region: null, + PostalCode: "44000", + Country: "France", + Salesperson: "Nancy Davolio", + OrderID: 10311, + OrderDate: new Date("4/14/2017"), + ShipperName: "Federal Shipping", + ProductID: 69, + ProductName: "Gudbrandsdalsost", + UnitPrice: 28.8000, + Quantity: 7, + Discontinued: false, + ExtendedPrice: 201.6000, + Freight: 24.6900 +}, { + ShipName: "Du monde entier", + ShipAddress: "67, rue des Cinquante Otages", + ShipCity: "Nantes", + ShipRegion: null, + ShipPostalCode: "44000", + ShipCountry: "France", + CustomerID: "DUMON", + CustomerName: "Du monde entier", + Address: "67, rue des Cinquante Otages", + City: "Nantes", + Region: null, + PostalCode: "44000", + Country: "France", + Salesperson: "Robert King", + OrderID: 10609, + OrderDate: new Date("6/14/2016"), + ShipperName: "United Package", + ProductID: 1, + ProductName: "Chai", + UnitPrice: 18.0000, + Quantity: 3, + Discontinued: false, + ExtendedPrice: 54.0000, + Freight: 1.8500 +}, { + ShipName: "Du monde entier", + ShipAddress: "67, rue des Cinquante Otages", + ShipCity: "Nantes", + ShipRegion: null, + ShipPostalCode: "44000", + ShipCountry: "France", + CustomerID: "DUMON", + CustomerName: "Du monde entier", + Address: "67, rue des Cinquante Otages", + City: "Nantes", + Region: null, + PostalCode: "44000", + Country: "France", + Salesperson: "Robert King", + OrderID: 10609, + OrderDate: new Date("4/14/2015"), + ShipperName: "United Package", + ProductID: 10, + ProductName: "Ikura", + UnitPrice: 31.0000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 310.0000, + Freight: 1.8500 +}, { + ShipName: "Du monde entier", + ShipAddress: "67, rue des Cinquante Otages", + ShipCity: "Nantes", + ShipRegion: null, + ShipPostalCode: "44000", + ShipCountry: "France", + CustomerID: "DUMON", + CustomerName: "Du monde entier", + Address: "67, rue des Cinquante Otages", + City: "Nantes", + Region: null, + PostalCode: "44000", + Country: "France", + Salesperson: "Robert King", + OrderID: 10609, + OrderDate: new Date("4/14/2016"), + ShipperName: "United Package", + ProductID: 21, + ProductName: "Sir Rodney's Scones", + UnitPrice: 10.0000, + Quantity: 6, + Discontinued: false, + ExtendedPrice: 60.0000, + Freight: 1.8500 +}, { + ShipName: "Du monde entier", + ShipAddress: "67, rue des Cinquante Otages", + ShipCity: "Nantes", + ShipRegion: null, + ShipPostalCode: "44000", + ShipCountry: "France", + CustomerID: "DUMON", + CustomerName: "Du monde entier", + Address: "67, rue des Cinquante Otages", + City: "Nantes", + Region: null, + PostalCode: "44000", + Country: "France", + Salesperson: "Andrew Fuller", + OrderID: 10683, + OrderDate: new Date("4/14/2017"), + ShipperName: "Speedy Express", + ProductID: 52, + ProductName: "Filo Mix", + UnitPrice: 7.0000, + Quantity: 9, + Discontinued: false, + ExtendedPrice: 63.0000, + Freight: 4.4000 +}, { + ShipName: "Du monde entier", + ShipAddress: "67, rue des Cinquante Otages", + ShipCity: "Nantes", + ShipRegion: null, + ShipPostalCode: "44000", + ShipCountry: "France", + CustomerID: "DUMON", + CustomerName: "Du monde entier", + Address: "67, rue des Cinquante Otages", + City: "Nantes", + Region: null, + PostalCode: "44000", + Country: "France", + Salesperson: "Robert King", + OrderID: 10890, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 17, + ProductName: "Alice Mutton", + UnitPrice: 39.0000, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 585.0000, + Freight: 32.7600 +}, { + ShipName: "Du monde entier", + ShipAddress: "67, rue des Cinquante Otages", + ShipCity: "Nantes", + ShipRegion: null, + ShipPostalCode: "44000", + ShipCountry: "France", + CustomerID: "DUMON", + CustomerName: "Du monde entier", + Address: "67, rue des Cinquante Otages", + City: "Nantes", + Region: null, + PostalCode: "44000", + Country: "France", + Salesperson: "Robert King", + OrderID: 10890, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 34, + ProductName: "Sasquatch Ale", + UnitPrice: 14.0000, + Quantity: 10, + Discontinued: false, + ExtendedPrice: 140.0000, + Freight: 32.7600 +}, { + ShipName: "Du monde entier", + ShipAddress: "67, rue des Cinquante Otages", + ShipCity: "Nantes", + ShipRegion: null, + ShipPostalCode: "44000", + ShipCountry: "France", + CustomerID: "DUMON", + CustomerName: "Du monde entier", + Address: "67, rue des Cinquante Otages", + City: "Nantes", + Region: null, + PostalCode: "44000", + Country: "France", + Salesperson: "Robert King", + OrderID: 10890, + OrderDate: new Date("2/14/2016"), + ShipperName: "Speedy Express", + ProductID: 41, + ProductName: "Jack's New England Clam Chowder", + UnitPrice: 9.6500, + Quantity: 14, + Discontinued: false, + ExtendedPrice: 135.1000, + Freight: 32.7600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10364, + OrderDate: new Date("4/14/2017"), + ShipperName: "Speedy Express", + ProductID: 69, + ProductName: "Gudbrandsdalsost", + UnitPrice: 28.8000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 864.0000, + Freight: 71.9700 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10364, + OrderDate: new Date("4/4/2016"), + ShipperName: "Speedy Express", + ProductID: 71, + ProductName: "Flotemysost", + UnitPrice: 17.2000, + Quantity: 5, + Discontinued: false, + ExtendedPrice: 86.0000, + Freight: 71.9700 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10400, + OrderDate: new Date("3/12/2016"), + ShipperName: "Federal Shipping", + ProductID: 29, + ProductName: "Th\u00fcringer Rostbratwurst", + UnitPrice: 99.0000, + Quantity: 21, + Discontinued: false, + ExtendedPrice: 2079.0000, + Freight: 83.9300 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10400, + OrderDate: new Date("4/14/2017"), + ShipperName: "Federal Shipping", + ProductID: 35, + ProductName: "Steeleye Stout", + UnitPrice: 14.4000, + Quantity: 35, + Discontinued: false, + ExtendedPrice: 504.0000, + Freight: 83.9300 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Nancy Davolio", + OrderID: 10400, + OrderDate: new Date("1/1/2017"), + ShipperName: "Federal Shipping", + ProductID: 49, + ProductName: "Maxilaku", + UnitPrice: 16.0000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 480.0000, + Freight: 83.9300 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Robert King", + OrderID: 10532, + OrderDate: new Date("4/22/2017"), + ShipperName: "Federal Shipping", + ProductID: 30, + ProductName: "Nord-Ost Matjeshering", + UnitPrice: 25.8900, + Quantity: 15, + Discontinued: false, + ExtendedPrice: 388.3500, + Freight: 74.4600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Robert King", + OrderID: 10532, + OrderDate: new Date("9/25/2017"), + ShipperName: "Federal Shipping", + ProductID: 66, + ProductName: "Louisiana Hot Spiced Okra", + UnitPrice: 17.0000, + Quantity: 24, + Discontinued: false, + ExtendedPrice: 408.0000, + Freight: 74.4600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 10726, + OrderDate: new Date("4/24/2015"), + ShipperName: "Speedy Express", + ProductID: 4, + ProductName: "Chef Anton's Cajun Seasoning", + UnitPrice: 22.0000, + Quantity: 25, + Discontinued: false, + ExtendedPrice: 550.0000, + Freight: 16.5600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 10726, + OrderDate: new Date("3/11/2017"), + ShipperName: "Speedy Express", + ProductID: 11, + ProductName: "Queso Cabrales", + UnitPrice: 21.0000, + Quantity: 5, + Discontinued: false, + ExtendedPrice: 105.0000, + Freight: 16.5600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 10987, + OrderDate: new Date("4/14/2016"), + ShipperName: "Speedy Express", + ProductID: 7, + ProductName: "Uncle Bob's Organic Dried Pears", + UnitPrice: 30.0000, + Quantity: 60, + Discontinued: false, + ExtendedPrice: 1800.0000, + Freight: 185.4800 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 10987, + OrderDate: new Date("1/24/2017"), + ShipperName: "Speedy Express", + ProductID: 43, + ProductName: "Ipoh Coffee", + UnitPrice: 46.0000, + Quantity: 6, + Discontinued: false, + ExtendedPrice: 276.0000, + Freight: 185.4800 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 10987, + OrderDate: new Date("3/31/2017"), + ShipperName: "Speedy Express", + ProductID: 72, + ProductName: "Mozzarella di Giovanni", + UnitPrice: 34.8000, + Quantity: 20, + Discontinued: false, + ExtendedPrice: 696.0000, + Freight: 185.4800 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 11024, + OrderDate: new Date("4/15/2018"), + ShipperName: "Speedy Express", + ProductID: 26, + ProductName: "Gumb\u00e4r Gummib\u00e4rchen", + UnitPrice: 31.2300, + Quantity: 12, + Discontinued: false, + ExtendedPrice: 374.7600, + Freight: 74.3600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 11024, + OrderDate: new Date("5/17/2017"), + ShipperName: "Speedy Express", + ProductID: 33, + ProductName: "Geitost", + UnitPrice: 2.5000, + Quantity: 30, + Discontinued: false, + ExtendedPrice: 75.0000, + Freight: 74.3600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 11024, + OrderDate: new Date("4/24/2016"), + ShipperName: "Speedy Express", + ProductID: 65, + ProductName: "Louisiana Fiery Hot Pepper Sauce", + UnitPrice: 21.0500, + Quantity: 21, + Discontinued: false, + ExtendedPrice: 442.0500, + Freight: 74.3600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Margaret Peacock", + OrderID: 11024, + OrderDate: new Date("6/24/2017"), + ShipperName: "Speedy Express", + ProductID: 71, + ProductName: "Flotemysost", + UnitPrice: 21.5000, + Quantity: 50, + Discontinued: false, + ExtendedPrice: 1075.0000, + Freight: 74.3600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 11056, + OrderDate: new Date("4/24/2017"), + ShipperName: "United Package", + ProductID: 7, + ProductName: "Uncle Bob's Organic Dried Pears", + UnitPrice: 30.0000, + Quantity: 40, + Discontinued: false, + ExtendedPrice: 1200.0000, + Freight: 278.9600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 11056, + OrderDate: new Date("3/24/2018"), + ShipperName: "United Package", + ProductID: 55, + ProductName: "P\u00e2t\u00e9 chinois", + UnitPrice: 24.0000, + Quantity: 35, + Discontinued: false, + ExtendedPrice: 840.0000, + Freight: 278.9600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Laura Callahan", + OrderID: 11056, + OrderDate: new Date("4/28/2018"), + ShipperName: "United Package", + ProductID: 60, + ProductName: "Camembert Pierrot", + UnitPrice: 34.0000, + Quantity: 50, + Discontinued: false, + ExtendedPrice: 1700.0000, + Freight: 278.9600 +}, { + ShipName: "Eastern Connection", + ShipAddress: "35 King George", + ShipCity: "London", + ShipRegion: null, + ShipPostalCode: "WX3 6FW", + ShipCountry: "UK", + CustomerID: "EASTC", + CustomerName: "Eastern Connection", + Address: "35 King George", + City: "London", + Region: null, + PostalCode: "WX3 6FW", + Country: "UK", + Salesperson: "Robert King", + OrderID: 11047, + OrderDate: new Date("4/24/2017"), + ShipperName: "Federal Shipping", + ProductID: 1, + ProductName: "Chai", + UnitPrice: 18.0000, + Quantity: 25, + Discontinued: true, + ExtendedPrice: 337.5000, + Freight: 46.6200 +}]; +/* tslint:enable */ From 5ab08a8872fc6200f65ebc7376082b2147fb17f7 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 11:56:34 +0300 Subject: [PATCH 055/106] chore(*): In case cell is merged cell placeholder, do not render content. --- .../src/lib/grids/cell.component.html | 2 ++ .../src/lib/grids/cell.component.ts | 13 +++++++++++ .../lib/grids/grid/grid-row.component.html | 23 +++++++++++-------- .../src/lib/grids/row.directive.ts | 2 +- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/cell.component.html b/projects/igniteui-angular/src/lib/grids/cell.component.html index b6f3f84656c..53095cebb22 100644 --- a/projects/igniteui-angular/src/lib/grids/cell.component.html +++ b/projects/igniteui-angular/src/lib/grids/cell.component.html @@ -57,6 +57,8 @@ } + + @if (column.dataType !== 'boolean' || (column.dataType === 'boolean' && this.formatter)) {
{ + if (this.isPlaceholder) { + return this.emptyCellTemplate; + } if (this.editMode && this.formGroup) { const inlineEditorTemplate = this.column.inlineEditorTemplate; return inlineEditorTemplate ? inlineEditorTemplate : this.inlineEditorTemplate; @@ -704,6 +714,9 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT @ViewChild('defaultCell', { read: TemplateRef, static: true }) protected defaultCellTemplate: TemplateRef; + @ViewChild('emptyCell', { read: TemplateRef, static: true }) + protected emptyCellTemplate: TemplateRef; + @ViewChild('defaultPinnedIndicator', { read: TemplateRef, static: true }) protected defaultPinnedIndicator: TemplateRef; diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index e8f2a00cab7..7c36e7d111a 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -28,11 +28,11 @@ @if (pinnedColumns.length > 0 && grid.isPinningToStart) { @for (col of pinnedColumns | igxNotGrouped; track trackPinnedColumn(col)) { @if (this.hasMergedCells) { -
@@ -44,11 +44,11 @@ } @if (this.hasMergedCells) { -
@@ -61,11 +61,11 @@ @if (pinnedColumns.length > 0 && !grid.isPinningToStart) { @for (col of pinnedColumns | igxNotGrouped; track trackPinnedColumn(col)) { @if (this.hasMergedCells) { -
@@ -135,6 +135,7 @@ [class.igx-grid__td--merged]="metaData?.cellMergeMeta.get(col.field)?.rowSpan > 1" [class.igx-grid__td--merged-selected]="isSelectionRoot(col)" [class.igx-grid__td--merged-hovered]="isHoveredRoot(col)" + [isPlaceholder]="!!this.metaData?.cellMergeMeta.get(col.field)?.root" class="igx-grid__td igx-grid__td--fw" [class.igx-grid__td--edited]="key | transactionState:col.field:grid.rowEditable:grid.transactions:grid.pipeTrigger:grid.gridAPI.crudService.cell:grid.gridAPI.crudService.row" [class.igx-grid__td--pinned]="col.pinned" @@ -170,6 +171,10 @@ 0; } /** From 229bcd149c7bb780b4cb189cd64a98cf29d71561 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 16:04:38 +0300 Subject: [PATCH 056/106] chore(*): Adjust indexes when there are pinned rows to top. --- .../src/lib/grids/grid-base.directive.ts | 5 +++-- .../src/lib/grids/grid/grid.component.html | 4 ++-- .../igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 7 +++++-- .../hierarchical-grid/hierarchical-grid.component.html | 4 ++-- .../src/lib/grids/tree-grid/tree-grid.component.html | 4 ++-- src/app/grid-cellMerging/grid-cellMerging.component.html | 9 ++++++--- src/app/grid-cellMerging/grid-cellMerging.component.ts | 7 ++++--- 7 files changed, 24 insertions(+), 16 deletions(-) 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 685cdba6c26..c16111026cb 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3697,7 +3697,7 @@ export abstract class IgxGridBaseDirective implements GridType, } protected getMergeCellOffset(rowData) { - const index = rowData.index; + const index = rowData.dataIndex; let offset = this.verticalScrollContainer.scrollPosition - this.verticalScrollContainer.getScrollForIndex(index); if (this.hasPinnedRecords && this.isRowPinningToTop) { offset -= this.pinnedRowHeight; @@ -3892,7 +3892,8 @@ export abstract class IgxGridBaseDirective implements GridType, if (rec.cellMergeMeta && // index + maxRowSpan is within view startIndex < (index + Math.max(...rec.cellMergeMeta.values().toArray().map(x => x.rowSpan)))) { - data.push({record: rec, index: index }); + const visibleIndex = this.isRowPinningToTop ? index + this.pinnedRecordsCount : index; + data.push({record: rec, index: visibleIndex, dataIndex: index }); } } this._mergedDataInView = data; 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 4e86643da62..3e6df0c38e9 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -63,7 +63,7 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:groupingExpressions:sortStrategy:id:pipeTrigger:true - | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:pipeTrigger; as pinnedData) { + | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData) { @if (pinnedData.length > 0) {
x - this.grid.pinnedRecordsCount); + } const result = DataUtil.merge(cloneArray(collection), colsToMerge, this.grid.mergeStrategy, activeRowIndexes, this.grid); return result; } 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 26e9b5efd5c..f097f45acf6 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 @@ -46,7 +46,7 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:[]:sortStrategy:id:pipeTrigger:true - | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:pipeTrigger; as pinnedData + | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
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 9e16f82611d..b77b0fa216c 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 @@ -47,7 +47,7 @@ | gridRowPinning:id:true:pipeTrigger | treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true - | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:pipeTrigger; as pinnedData + | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index bc53e1cb9fa..63c02663d1d 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -45,7 +45,10 @@

Grid with cell merge

- + + + Value: {{val}},Index: {{cell.row.index}} + @@ -69,9 +72,9 @@

Grid with cell merge

- + diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index d8cb3919a15..45130d806d2 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -38,8 +38,8 @@ import { INVOICE_DATA } from '../shared/invoiceData'; IgxColumnComponent, IgxGridComponent, IgxPaginatorComponent, - // IgxActionStripComponent, - // IgxGridPinningActionsComponent, + IgxActionStripComponent, + IgxGridPinningActionsComponent, IgxGridToolbarComponent, IgxGridToolbarActionsComponent, IgxGridToolbarPinningComponent, @@ -51,7 +51,8 @@ import { INVOICE_DATA } from '../shared/invoiceData'; IgxPrefixDirective, IgxSuffixDirective, IgxIconComponent, - IgxInputDirective + IgxInputDirective, + IgxCellTemplateDirective ] }) export class GridCellMergingComponent { From e64ee0199df612f5d2ae2cc093ec01b082a2626a Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 16:52:08 +0300 Subject: [PATCH 057/106] chore(*): Fix templates in hgrid and tgrid. --- .../hierarchical-row.component.html | 6 +++--- .../lib/grids/tree-grid/tree-grid-row.component.html | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html index 1f0b9e60507..913bf19f5a9 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html @@ -55,11 +55,11 @@ @if (this.hasMergedCells) { -
diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html index 16ccc6ae995..2832120f68c 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html @@ -24,11 +24,11 @@ } @if (this.hasMergedCells) { -
@@ -60,11 +60,11 @@ @for (col of pinnedColumns | igxNotGrouped; track trackPinnedColumn(col)) { @if (this.hasMergedCells) { -
From 238390f2abc2d738d95635aa75db24486d079985 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 17:07:18 +0300 Subject: [PATCH 058/106] chore(*): Update external merge container on data changing. --- .../src/lib/grids/grid-base.directive.ts | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) 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 c16111026cb..a5f9995bd02 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3859,6 +3859,8 @@ export abstract class IgxGridBaseDirective implements GridType, $event.containerSize = this.calcHeight; } this.evaluateLoadingState(); + this.updateMergedData(); + this.cdr.detectChanges(); }); this.verticalScrollContainer.scrollbarVisibilityChanged.pipe(filter(() => !this._init), destructor).subscribe(() => { @@ -3882,22 +3884,7 @@ export abstract class IgxGridBaseDirective implements GridType, }); this.verticalScrollContainer.chunkPreload.pipe(filter(() => !this._init), destructor).subscribe(() => { - // recalc merged data - if (this.columnsToMerge.length > 0) { - const startIndex = this.verticalScrollContainer.state.startIndex; - const prevDataView = this.verticalScrollContainer.igxForOf?.slice(0, startIndex); - const data = []; - for (let index = 0; index < startIndex; index++) { - const rec = prevDataView[index]; - if (rec.cellMergeMeta && - // index + maxRowSpan is within view - startIndex < (index + Math.max(...rec.cellMergeMeta.values().toArray().map(x => x.rowSpan)))) { - const visibleIndex = this.isRowPinningToTop ? index + this.pinnedRecordsCount : index; - data.push({record: rec, index: visibleIndex, dataIndex: index }); - } - } - this._mergedDataInView = data; - } + this.updateMergedData(); }); // notifier for column autosize requests @@ -8137,4 +8124,25 @@ export abstract class IgxGridBaseDirective implements GridType, return recreateTreeFromFields(value, this._columns) as IFilteringExpressionsTree; } } + + private updateMergedData(){ + // recalc merged data + if (this.columnsToMerge.length > 0) { + const startIndex = this.verticalScrollContainer.state.startIndex; + const prevDataView = this.verticalScrollContainer.igxForOf?.slice(0, startIndex); + const data = []; + for (let index = 0; index < startIndex; index++) { + const rec = prevDataView[index]; + if (rec.cellMergeMeta && + // index + maxRowSpan is within view + startIndex < (index + Math.max(...rec.cellMergeMeta.values().toArray().map(x => x.rowSpan)))) { + const visibleIndex = this.isRowPinningToTop ? index + this.pinnedRecordsCount : index; + data.push({record: rec, index: visibleIndex, dataIndex: index }); + } + } + this._mergedDataInView = data; + console.log(data); + //this._activeRowIndexes = null; + } + } } From c071635c28f5d55bbab632aa84eaa1783e248405 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 17:25:28 +0300 Subject: [PATCH 059/106] chore(*): Adjust selection check for pinned row root. --- projects/igniteui-angular/src/lib/grids/grid-base.directive.ts | 2 -- projects/igniteui-angular/src/lib/grids/row.directive.ts | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) 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 a5f9995bd02..d75a06544ef 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -8141,8 +8141,6 @@ export abstract class IgxGridBaseDirective implements GridType, } } this._mergedDataInView = data; - console.log(data); - //this._activeRowIndexes = null; } } } diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index 1e6359b0433..45fa0c88cf7 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -626,7 +626,8 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { const mergeMeta = this.metaData?.cellMergeMeta; const rowCount = mergeMeta?.get(col.field)?.rowSpan; if (mergeMeta && rowCount > 1) { - const indexInData = this.pinned && this.grid.isRowPinningToTop ? this.index - this.grid.pinnedRecordsCount : this.index; + const isPinned = this.pinned && this.disabled; + const indexInData = isPinned && this.grid.isRowPinningToTop ? this.index - this.grid.pinnedRecordsCount : this.index; const range = this.grid.verticalScrollContainer.igxForOf.slice(indexInData, indexInData + rowCount); const inRange = range.filter(x => this.selectionService.isRowSelected(this.grid.primaryKey ? (x.recordRef || x)[this.grid.primaryKey] : (x.recordRef || x))).length > 0; return inRange; From 9f1ffd046da2d5fad7bb883454b6092742e7e7e7 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 17:34:46 +0300 Subject: [PATCH 060/106] chore(*): Fix more indexes due to row pinning. --- projects/igniteui-angular/src/lib/grids/row.directive.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index 45fa0c88cf7..fd86cd9a4d2 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -626,8 +626,8 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { const mergeMeta = this.metaData?.cellMergeMeta; const rowCount = mergeMeta?.get(col.field)?.rowSpan; if (mergeMeta && rowCount > 1) { - const isPinned = this.pinned && this.disabled; - const indexInData = isPinned && this.grid.isRowPinningToTop ? this.index - this.grid.pinnedRecordsCount : this.index; + const isPinned = this.pinned && !this.disabled; + const indexInData = this.grid.isRowPinningToTop && !isPinned ? this.index - this.grid.pinnedRecordsCount : this.index; const range = this.grid.verticalScrollContainer.igxForOf.slice(indexInData, indexInData + rowCount); const inRange = range.filter(x => this.selectionService.isRowSelected(this.grid.primaryKey ? (x.recordRef || x)[this.grid.primaryKey] : (x.recordRef || x))).length > 0; return inRange; From 0ae58e023ad0e92c01dae7e67c1582a85e88375c Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 17:56:15 +0300 Subject: [PATCH 061/106] chore(*): Fix tgrid check. --- .../lib/grids/tree-grid/tree-grid-row.component.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html index 2832120f68c..8208ac102ac 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html @@ -24,11 +24,11 @@ } @if (this.hasMergedCells) { -
@@ -60,11 +60,11 @@ @for (col of pinnedColumns | igxNotGrouped; track trackPinnedColumn(col)) { @if (this.hasMergedCells) { -
From 2267e03a87239d09d0f79ee27c7817a534684e01 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 18:02:31 +0300 Subject: [PATCH 062/106] chore(*): Clear active row indexes when selection is cleared. --- .../igniteui-angular/src/lib/grids/grid-base.directive.ts | 5 +++++ 1 file changed, 5 insertions(+) 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 d75a06544ef..99f51a28e23 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -2926,6 +2926,7 @@ export abstract class IgxGridBaseDirective implements GridType, this._cellSelectionMode = selectionMode; // if (this.gridAPI.grid) { this.selectionService.clear(true); + this._activeRowIndexes = null; this.notifyChanges(); // } } @@ -4120,6 +4121,7 @@ export abstract class IgxGridBaseDirective implements GridType, .pipe(takeUntil(this.destroy$)) .subscribe(() => { this.selectionService.clear(true); + this._activeRowIndexes = null; this.crudService.endEdit(false); this.pipeTrigger++; this.navigateTo(0); @@ -4130,6 +4132,7 @@ export abstract class IgxGridBaseDirective implements GridType, .pipe(takeUntil(this.destroy$)) .subscribe(() => { this.selectionService.clear(true); + this._activeRowIndexes = null; this.page = 0; this.crudService.endEdit(false); this.notifyChanges(); @@ -5855,6 +5858,7 @@ export abstract class IgxGridBaseDirective implements GridType, */ public clearCellSelection(): void { this.selectionService.clear(true); + this._activeRowIndexes = null; this.notifyChanges(); } @@ -8114,6 +8118,7 @@ export abstract class IgxGridBaseDirective implements GridType, private clearActiveNode() { this.navigation.lastActiveNode = this.navigation.activeNode; this.navigation.activeNode = {} as IActiveNode; + this._activeRowIndexes = null; this.notifyChanges(); } From 6178a239c24e3dcf67fb0aecf160a64413ed2667 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 18:13:50 +0300 Subject: [PATCH 063/106] chore(*): Adjust selection check to use pinned view if row is pinned. --- projects/igniteui-angular/src/lib/grids/row.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index fd86cd9a4d2..4b9cf7df371 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -628,7 +628,7 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { if (mergeMeta && rowCount > 1) { const isPinned = this.pinned && !this.disabled; const indexInData = this.grid.isRowPinningToTop && !isPinned ? this.index - this.grid.pinnedRecordsCount : this.index; - const range = this.grid.verticalScrollContainer.igxForOf.slice(indexInData, indexInData + rowCount); + const range = isPinned ? this.grid.pinnedDataView.slice(indexInData, indexInData + rowCount) : this.grid.verticalScrollContainer.igxForOf.slice(indexInData, indexInData + rowCount); const inRange = range.filter(x => this.selectionService.isRowSelected(this.grid.primaryKey ? (x.recordRef || x)[this.grid.primaryKey] : (x.recordRef || x))).length > 0; return inRange; } From d044219e91ee57504edac746b377a72ad130a353 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 18:25:33 +0300 Subject: [PATCH 064/106] chore(*): Fix scrollbar disappearing on data changing. --- projects/igniteui-angular/src/lib/grids/grid-base.directive.ts | 1 - 1 file changed, 1 deletion(-) 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 99f51a28e23..67299234441 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3861,7 +3861,6 @@ export abstract class IgxGridBaseDirective implements GridType, } this.evaluateLoadingState(); this.updateMergedData(); - this.cdr.detectChanges(); }); this.verticalScrollContainer.scrollbarVisibilityChanged.pipe(filter(() => !this._init), destructor).subscribe(() => { From b8a77e2a0ab2ad001cdd8fe63e87b363a52944c1 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 28 Jul 2025 18:34:59 +0300 Subject: [PATCH 065/106] chore(*): Notify changes after merge data is updated. --- projects/igniteui-angular/src/lib/grids/grid-base.directive.ts | 1 + 1 file changed, 1 insertion(+) 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 67299234441..d862abbfed9 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -8145,6 +8145,7 @@ export abstract class IgxGridBaseDirective implements GridType, } } this._mergedDataInView = data; + this.notifyChanges(); } } } From 03511667adbe6e53e070b09f1433120747204fdc Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 29 Jul 2025 11:29:27 +0300 Subject: [PATCH 066/106] chore(*): Apply review comments on styles. --- .../components/grid/_grid-component.scss | 3 ++ .../styles/components/grid/_grid-theme.scss | 34 +++---------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss index 92ab66cf5c9..fa2c8d91ac0 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss @@ -300,6 +300,9 @@ @extend %igx-grid__td--merged !optional; } + @include e(td, $mods: (merged-selected, merged-hovered)) { + @extend %igx-grid__td--merged-selected-hovered !optional; + } @include e(td, $m: merged-selected) { @extend %igx-grid__td--merged-selected !optional; diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index 4537aa86c68..52fcb901fee 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1849,27 +1849,6 @@ color: var-get($theme, 'row-selected-text-color'); background: var-get($theme, 'row-selected-background'); - &%grid-row--mrl { - %grid-mrl-block { - %igx-grid__td--merged { - color: var-get($theme, 'row-selected-text-color'); - background: var-get($theme, 'row-selected-background'); - - &:hover { - background: var-get($theme, 'row-selected-hover-background'); - color: var-get($theme, 'row-selected-hover-text-color'); - } - - &%igx-grid__td--merged-hovered { - background: var-get($theme, 'row-selected-hover-background'); - color: var-get($theme, 'row-selected-hover-text-color'); - } - } - } - } - - - %grid-cell--selected, %grid-cell--pinned-selected { color: var-get($theme, 'cell-selected-within-text-color'); @@ -1925,21 +1904,16 @@ %igx-grid__td--merged-selected { color: var-get($theme, 'row-selected-text-color'); background: var-get($theme, 'row-selected-background'); - - &%igx-grid__td--merged-hovered { - background: var-get($theme, 'row-selected-hover-background'); - color: var-get($theme, 'row-selected-hover-text-color'); - } } %igx-grid__td--merged-hovered { background: var-get($theme, 'row-hover-background'); color: var-get($theme, 'row-hover-text-color'); + } - &%igx-grid__td--merged-selected { - background: var-get($theme, 'row-selected-hover-background'); - color: var-get($theme, 'row-selected-hover-text-color'); - } + %igx-grid__td--merged-selected-hovered { + background: var-get($theme, 'row-selected-hover-background'); + color: var-get($theme, 'row-selected-hover-text-color'); } %igx-grid__tr--deleted { From 9cc56b68f48c102f3e2a249df2db1eccfc0a84a1 Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 29 Jul 2025 11:54:41 +0300 Subject: [PATCH 067/106] chore(*): Extract key from tree grid record. --- .../src/lib/grids/row.directive.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index 4b9cf7df371..cb7a50009b0 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -629,7 +629,7 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { const isPinned = this.pinned && !this.disabled; const indexInData = this.grid.isRowPinningToTop && !isPinned ? this.index - this.grid.pinnedRecordsCount : this.index; const range = isPinned ? this.grid.pinnedDataView.slice(indexInData, indexInData + rowCount) : this.grid.verticalScrollContainer.igxForOf.slice(indexInData, indexInData + rowCount); - const inRange = range.filter(x => this.selectionService.isRowSelected(this.grid.primaryKey ? (x.recordRef || x)[this.grid.primaryKey] : (x.recordRef || x))).length > 0; + const inRange = range.filter(x => this.selectionService.isRowSelected(this.extractRecordKey(x))).length > 0; return inRange; } return false; @@ -646,6 +646,18 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { return false; } + protected extractRecordKey(rec: any) { + let recData = rec; + if (this.grid.isRecordMerged(recData)) { + recData = rec.recordRef; + } + + if(this.grid.isTreeRow && this.grid.isTreeRow(recData)){ + recData = recData.data; + } + return this.grid.primaryKey ? recData[this.grid.primaryKey] : recData; + } + protected getRowHeight() { const indexInData = this.grid.verticalScrollContainer.igxForOf.indexOf(this.metaData); const size = this.grid.verticalScrollContainer.getSizeAt(indexInData) - 1; From cbf4a1c1ab37f129bc891aa4438af4af4ed31e1c Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 29 Jul 2025 12:11:56 +0300 Subject: [PATCH 068/106] chore(*): Update merge indexes when in a paged grid context. --- .../igniteui-angular/src/lib/grids/grid-base.directive.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 d862abbfed9..da7ec8310f3 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -7985,7 +7985,12 @@ export abstract class IgxGridBaseDirective implements GridType, const searchText = caseSensitive ? this._lastSearchInfo.searchText : this._lastSearchInfo.searchText.toLowerCase(); let data = this.filteredSortedData; if (this.hasCellsToMerge) { - data = DataUtil.merge(cloneArray(this.filteredSortedData), this.columnsToMerge, this.mergeStrategy, this.activeRowIndexes, this); + let indexes = this.activeRowIndexes; + if (this.page > 0) { + indexes = indexes.map(x => this.perPage * this.page + x ); + } + + data = DataUtil.merge(cloneArray(this.filteredSortedData), this.columnsToMerge, this.mergeStrategy, indexes, this); } const columnItems = this.visibleColumns.filter((c) => !c.columnGroup).sort((c1, c2) => c1.visibleIndex - c2.visibleIndex); const columnsPathParts = columnItems.map(col => columnFieldPath(col.field)); From e4916812aaac1dd4e73804f1e5b6ecfdfea7d9d0 Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 29 Jul 2025 13:05:06 +0300 Subject: [PATCH 069/106] chore(*): Update size if repaint was requested. Optimize a bit index calc. --- .../igniteui-angular/src/lib/grids/row.directive.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts index cb7a50009b0..24d4b0989b7 100644 --- a/projects/igniteui-angular/src/lib/grids/row.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts @@ -613,8 +613,8 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { protected getMergeCellSpan(col: ColumnType) { const rowCount = this.metaData.cellMergeMeta.get(col.field).rowSpan; let sizeSpans = ""; - const indexInData = this.pinned ? this.grid.getInitialPinnedIndex(this.data): - this.grid.verticalScrollContainer.igxForOf.indexOf(this.metaData); + const isPinned = this.pinned && !this.disabled; + const indexInData = this.grid.isRowPinningToTop && !isPinned ? this.index - this.grid.pinnedRecordsCount : this.index; for (let index = indexInData; index < indexInData + rowCount; index++) { const size = this.grid.verticalScrollContainer.getSizeAt(index); sizeSpans += size + 'px '; @@ -659,7 +659,12 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy { } protected getRowHeight() { - const indexInData = this.grid.verticalScrollContainer.igxForOf.indexOf(this.metaData); + const isPinned = this.pinned && !this.disabled; + const indexInData = this.grid.isRowPinningToTop && !isPinned ? this.index - this.grid.pinnedRecordsCount : this.index; + if ((this.grid as any)._cdrRequests) { + // recalc size if repaint is requested. + this.grid.verticalScrollContainer.recalcUpdateSizes(); + } const size = this.grid.verticalScrollContainer.getSizeAt(indexInData) - 1; return size || this.grid.rowHeight; } From 5d2a5ce4947289df1ba657b22b60d81d13f63be5 Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 29 Jul 2025 14:35:48 +0300 Subject: [PATCH 070/106] chore(*): Remove unnecessary inherit that overrides selection in pin area. --- .../src/lib/core/styles/components/grid/_grid-theme.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index 52fcb901fee..b0eeb099572 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1948,7 +1948,6 @@ %grid-cell--pinned { position: relative; - background: inherit; z-index: 9999; } From db23777b9333baeb337f0dd5bd4334d2b246a453 Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 29 Jul 2025 16:20:00 +0300 Subject: [PATCH 071/106] chore(*): Fix timing issue between activation and drag selection. --- .../igniteui-angular/src/lib/grids/grid-base.directive.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 da7ec8310f3..d9138a16862 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3897,7 +3897,10 @@ export abstract class IgxGridBaseDirective implements GridType, this._firstAutoResize = false; }); - this.activeNodeChange.pipe(filter(() => !this._init), destructor).subscribe(() => { + this.activeNodeChange.pipe( + throttleTime(0, this.platform.isBrowser ? animationFrameScheduler : undefined, { leading: false, trailing: true }), + destructor + ).subscribe(() => { this._activeRowIndexes = null; if (this.hasCellsToMerge) { this.refreshSearch(); From 3f7c27e35890f10ae4741e66a922664dd8f5a5c2 Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 29 Jul 2025 16:31:01 +0300 Subject: [PATCH 072/106] chore(*): Make activation and merge tests async. --- .../src/lib/grids/grid/cell-merge.spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts index 0493bdef316..963ddd7726c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -414,7 +414,7 @@ describe('IgxGrid - Cell merging #grid', () => { describe('Activation', () => { - it('should interrupt merge sequence so that active row has no merging.', () => { + it('should interrupt merge sequence so that active row has no merging.', async() => { const col = grid.getColumnByName('ProductName'); GridFunctions.verifyColumnMergedState(grid, col, [ { value: 'Ignite UI for JavaScript', span: 2 }, @@ -428,6 +428,7 @@ describe('IgxGrid - Cell merging #grid', () => { const row1 = grid.rowList.toArray()[0]; UIInteractions.simulateClickAndSelectEvent(row1.cells.toArray()[1].nativeElement); + await wait(1); fix.detectChanges(); GridFunctions.verifyColumnMergedState(grid, col, [ @@ -451,7 +452,7 @@ describe('IgxGrid - Cell merging #grid', () => { fix.detectChanges(); }); - it('should edit the individual row values for the active row.', () => { + it('should edit the individual row values for the active row.', async() => { const col = grid.getColumnByName('ProductName'); grid.rowEditable = true; fix.detectChanges(); @@ -459,6 +460,7 @@ describe('IgxGrid - Cell merging #grid', () => { const row = grid.gridAPI.get_row_by_index(0); const cell = grid.gridAPI.get_cell_by_index(0, 'ProductName'); UIInteractions.simulateDoubleClickAndSelectEvent(cell.nativeElement); + await wait(1); fix.detectChanges(); expect(row.inEditMode).toBe(true); @@ -652,13 +654,14 @@ describe('IgxGrid - Cell merging #grid', () => { expect(activeHighlight[0].closest("igx-grid-cell")).toBe(cell0); }); - it('should update matches if a cell becomes unmerged.', () => { + it('should update matches if a cell becomes unmerged.', async() => { let matches = grid.findNext('JavaScript'); fix.detectChanges(); expect(matches).toBe(2); UIInteractions.simulateClickAndSelectEvent(grid.gridAPI.get_cell_by_index(0, 'ProductName').nativeElement); + await wait(1); fix.detectChanges(); matches = grid.findNext('JavaScript'); From 2bbd9b82f023adb56bddeffe99f66a94814cb8e6 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 30 Jul 2025 13:03:54 +0300 Subject: [PATCH 073/106] chore(*): Approximate click position in merge cell to activate closest row. --- .../src/lib/grids/cell.component.ts | 18 ++++++++++++++++++ .../src/lib/grids/grid/grid-row.component.html | 2 ++ .../hierarchical-row.component.html | 2 ++ .../src/lib/grids/row.directive.ts | 8 ++++++++ .../tree-grid/tree-grid-row.component.html | 4 ++++ 5 files changed, 34 insertions(+) diff --git a/projects/igniteui-angular/src/lib/grids/cell.component.ts b/projects/igniteui-angular/src/lib/grids/cell.component.ts index 29c9b8c8444..1a8acc786a8 100644 --- a/projects/igniteui-angular/src/lib/grids/cell.component.ts +++ b/projects/igniteui-angular/src/lib/grids/cell.component.ts @@ -151,6 +151,11 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT @Input() public isPlaceholder: boolean; + /** + Gets whether this cell is a merged cell. + */ + @Input() + public isMerged: boolean; /** * @hidden @@ -1016,6 +1021,19 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT * @internal */ public pointerdown = (event: PointerEvent) => { + + if (this.isMerged) { + // need an approximation of where in the cell the user clicked to get actual index to be activated. + const scrollOffset = this.grid.verticalScrollContainer.scrollPosition + (event.y - this.grid.tbody.nativeElement.getBoundingClientRect().y); + const targetRowIndex = this.grid.verticalScrollContainer.getIndexAtScroll(scrollOffset); + if (targetRowIndex != this.rowIndex) { + const row = this.grid.rowList.toArray().find(x => x.index === targetRowIndex); + const actualTarget = row.cells.find(x => x.column === this.column); + actualTarget.pointerdown(event); + return; + } + } + if (this.cellSelectionMode !== GridSelectionMode.multiple) { this.activate(event); return; diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index 7c36e7d111a..75f3df188dc 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -135,6 +135,7 @@ [class.igx-grid__td--merged]="metaData?.cellMergeMeta.get(col.field)?.rowSpan > 1" [class.igx-grid__td--merged-selected]="isSelectionRoot(col)" [class.igx-grid__td--merged-hovered]="isHoveredRoot(col)" + [isMerged]="metaData?.cellMergeMeta.get(col.field)?.rowSpan > 1" [isPlaceholder]="!!this.metaData?.cellMergeMeta.get(col.field)?.root" class="igx-grid__td igx-grid__td--fw" [class.igx-grid__td--edited]="key | transactionState:col.field:grid.rowEditable:grid.transactions:grid.pipeTrigger:grid.gridAPI.crudService.cell:grid.gridAPI.crudService.row" @@ -174,6 +175,7 @@ [class.igx-grid__td--merged]="metaData?.cellMergeMeta.get(col.field)?.rowSpan > 1" [class.igx-grid__td--merged-selected]="isSelectionRoot(col)" [class.igx-grid__td--merged-hovered]="isHoveredRoot(col)" + [isMerged]="metaData?.cellMergeMeta.get(col.field)?.rowSpan > 1" [isPlaceholder]="!!this.metaData?.cellMergeMeta.get(col.field)?.root" [class.igx-grid__td--pinned]="col.pinned" class="igx-grid__td igx-grid__td--fw igx-grid__td--tree-cell" diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html index 913bf19f5a9..c8192215d8b 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-row.component.html @@ -123,6 +123,8 @@ x.index === targetRowIndex); + row.onClick(event); + return; + } + } this.grid.rowClick.emit({ row: this, event diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html index 8208ac102ac..b2a4ed217ec 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.html @@ -81,6 +81,8 @@ [class.igx-grid__td--merged-selected]="isSelectionRoot(col)" [class.igx-grid__td--merged-hovered]="isHoveredRoot(col)" [class.igx-grid__td--pinned]="col.pinned" + [isMerged]="metaData?.cellMergeMeta.get(col.field)?.rowSpan > 1" + [isPlaceholder]="!!this.metaData?.cellMergeMeta.get(col.field)?.root" class="igx-grid__td igx-grid__td--fw" [class.igx-grid__td--edited]="key | transactionState:col.field:grid.rowEditable:grid.transactions:grid.pipeTrigger:grid.gridAPI.crudService.cell:grid.gridAPI.crudService.row" [class.igx-grid__td--number]="col.dataType === 'number' || col.dataType === 'percent' || col.dataType === 'currency'" @@ -115,6 +117,8 @@ [class.igx-grid__td--merged]="metaData?.cellMergeMeta.get(col.field)?.rowSpan > 1" [class.igx-grid__td--merged-selected]="isSelectionRoot(col)" [class.igx-grid__td--merged-hovered]="isHoveredRoot(col)" + [isMerged]="metaData?.cellMergeMeta.get(col.field)?.rowSpan > 1" + [isPlaceholder]="!!this.metaData?.cellMergeMeta.get(col.field)?.root" [class.igx-grid__td--pinned]="col.pinned" [class.igx-grid__td--edited]="key | transactionState:col.field:grid.rowEditable:grid.transactions:grid.pipeTrigger:grid.gridAPI.crudService.cell:grid.gridAPI.crudService.row" [class.igx-grid__td--number]="(col.dataType === 'number' || col.dataType === 'percent' || col.dataType === 'currency') && col.visibleIndex !== 0" From bcae71b27ac4bbbbc287a3e089fad67d98f7a299 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 30 Jul 2025 14:20:38 +0300 Subject: [PATCH 074/106] chore(*): Pass clientY when simulating pointer events in tests. --- .../igniteui-angular/src/lib/test-utils/ui-interactions.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/igniteui-angular/src/lib/test-utils/ui-interactions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/ui-interactions.spec.ts index 0b72f5a3c7b..eb470e8137f 100644 --- a/projects/igniteui-angular/src/lib/test-utils/ui-interactions.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/ui-interactions.spec.ts @@ -345,6 +345,7 @@ export class UIInteractions { cancelable: true, pointerId: 1, buttons: 1, + clientY: element.getBoundingClientRect().y, button: eventName === 'pointerenter' ? -1 : 0, shiftKey, ctrlKey From 2d6b4431eed4c68868df4ae1b7aeaa8cc70a6379 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 31 Jul 2025 11:56:38 +0300 Subject: [PATCH 075/106] chore(*): Fix background styles when pinned and merged. --- .../src/lib/core/styles/components/grid/_grid-theme.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index b0eeb099572..2197341510d 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1903,11 +1903,11 @@ %igx-grid__td--merged-selected { color: var-get($theme, 'row-selected-text-color'); - background: var-get($theme, 'row-selected-background'); + background: var-get($theme, 'row-selected-background') !important; } %igx-grid__td--merged-hovered { - background: var-get($theme, 'row-hover-background'); + background: var-get($theme, 'row-hover-background') !important; color: var-get($theme, 'row-hover-text-color'); } @@ -1948,6 +1948,7 @@ %grid-cell--pinned { position: relative; + background: inherit; z-index: 9999; } From f69b105422b5ad5b8872801cfe3924be714f85a0 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 31 Jul 2025 13:18:49 +0300 Subject: [PATCH 076/106] chore(*): Fix background styles when merged, hovered and selected. --- .../src/lib/core/styles/components/grid/_grid-theme.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index ef4af4af632..de61ec6aacb 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1356,7 +1356,7 @@ } %igx-grid__td--merged-selected-hovered { - background: var-get($theme, 'row-selected-hover-background'); + background: var-get($theme, 'row-selected-hover-background') !important; color: var-get($theme, 'row-selected-hover-text-color'); } From 13b4c24a20ade84ecdfcdea50119b4f785486744 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 31 Jul 2025 17:31:15 +0300 Subject: [PATCH 077/106] chore(*): Add merge strategy to pipe trigger so that it can be changed runtime. --- .../igniteui-angular/src/lib/grids/grid/grid.component.html | 4 ++-- projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 5 +++-- .../grids/hierarchical-grid/hierarchical-grid.component.html | 4 ++-- .../src/lib/grids/tree-grid/tree-grid.component.html | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) 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 3e6df0c38e9..727aee7cf5a 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -63,7 +63,7 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:groupingExpressions:sortStrategy:id:pipeTrigger:true - | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData) { + | gridCellMerge:visibleColumns:cellMergeMode:mergeStrategy:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData) { @if (pinnedData.length > 0) {
x - this.grid.pinnedRecordsCount); } - const result = DataUtil.merge(cloneArray(collection), colsToMerge, this.grid.mergeStrategy, activeRowIndexes, this.grid); + const result = DataUtil.merge(cloneArray(collection), colsToMerge, mergeStrategy, activeRowIndexes, this.grid); return result; } } 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 f097f45acf6..b42fa1064b8 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 @@ -46,7 +46,7 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:[]:sortStrategy:id:pipeTrigger:true - | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData + | gridCellMerge:visibleColumns:cellMergeMode:mergeStrategy:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
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 b77b0fa216c..40eeeb3229f 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 @@ -47,7 +47,7 @@ | gridRowPinning:id:true:pipeTrigger | treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true - | gridCellMerge:visibleColumns:cellMergeMode:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData + | gridCellMerge:visibleColumns:cellMergeMode:mergeStrategy:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
From 05e42c644d5acb6bba16d06a246c5668a41de0a6 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 31 Jul 2025 17:51:07 +0300 Subject: [PATCH 078/106] chore(*): Change detect on runtime strategy change. --- .../src/lib/grids/grid-base.directive.ts | 3 +++ src/app/grid-cellMerging/grid-cellMerging.component.html | 2 +- src/app/grid-cellMerging/grid-cellMerging.component.ts | 9 +++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) 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 d9138a16862..b524a4541dd 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -2510,6 +2510,9 @@ export abstract class IgxGridBaseDirective implements GridType, } public set mergeStrategy(value) { this._mergeStrategy = value; + if (!this._init) { + this.cdr.detectChanges(); + } } /** diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 63c02663d1d..29b78e55884 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -111,7 +111,7 @@

Hierarchical grid with cell merge

Tree grid with cell merge

- + diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index 45130d806d2..3374c5e606f 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -1,6 +1,7 @@ import { Component, HostBinding, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { + DefaultTreeGridMergeStrategy, IgxActionStripComponent, IgxButtonDirective, IgxCellTemplateDirective, @@ -65,6 +66,14 @@ export class GridCellMergingComponent { @ViewChild('grid1', { static: true }) public grid: IgxGridComponent; public data = INVOICE_DATA; + public toggleStrategy() { + if (this.treeGridMergeStrategy instanceof ByLevelTreeGridMergeStrategy) { + this.treeGridMergeStrategy = new DefaultTreeGridMergeStrategy(); + } else { + this.treeGridMergeStrategy = new ByLevelTreeGridMergeStrategy(); + } + } + public searchKeyDown(ev) { if (ev.key === 'Enter' || ev.key === 'ArrowDown' || ev.key === 'ArrowRight') { ev.preventDefault(); From a06132a43111b44c1bbe5a8c6a374f92447844a1 Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 1 Aug 2025 12:12:29 +0300 Subject: [PATCH 079/106] fix(*): Fix positioning in pin right scenario. Extract styles in class. --- .../src/lib/core/styles/components/grid/_grid-component.scss | 4 ++++ .../src/lib/core/styles/components/grid/_grid-theme.scss | 5 +++++ .../igniteui-angular/src/lib/grids/grid/grid.component.html | 2 +- .../grids/hierarchical-grid/hierarchical-grid.component.html | 2 +- .../src/lib/grids/tree-grid/tree-grid.component.html | 2 +- 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss index fa2c8d91ac0..8b0f9b88af0 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss @@ -272,6 +272,10 @@ @extend %igx-grid__tr--pinned !optional; } + @include e(tr, $m: merged) { + @extend %igx-grid__tr--merged !optional; + } + @include e(tr, $m: pinned-top) { @extend %igx-grid__tr--pinned-top !optional; } diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index de61ec6aacb..25f0fc3addf 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1340,6 +1340,11 @@ } } + %igx-grid__tr--merged { + position: absolute; + width: 100%; + } + %igx-grid__td--merged { z-index: 1; grid-row: 1 / -1; 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 727aee7cf5a..830d3a95c06 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -50,7 +50,7 @@ @for (rowData of mergedDataInView; track rowData.record;) { - 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 b42fa1064b8..adca8662b94 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 @@ -33,7 +33,7 @@ } @for (rowData of mergedDataInView; track rowData.record;) { - 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 40eeeb3229f..c3ecb711690 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 @@ -32,7 +32,7 @@ } @for (rowData of mergedDataInView; track rowData.record;) { - From 55288673177439902d2b4680bae638c84fbc3850 Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 1 Aug 2025 12:37:39 +0300 Subject: [PATCH 080/106] chore(*): Fix border styles for cell merging. --- .../src/lib/core/styles/components/grid/_grid-component.scss | 4 ++++ .../src/lib/core/styles/components/grid/_grid-theme.scss | 4 ++++ projects/igniteui-angular/src/lib/grids/common/pipes.ts | 1 + .../igniteui-angular/src/lib/grids/grid/grid.component.html | 2 +- .../grids/hierarchical-grid/hierarchical-grid.component.html | 2 +- .../src/lib/grids/tree-grid/tree-grid.component.html | 2 +- 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss index 8b0f9b88af0..91b864e6c87 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss @@ -276,6 +276,10 @@ @extend %igx-grid__tr--merged !optional; } + @include e(tr, $m: merged-top) { + @extend %igx-grid__tr--merged-top !optional; + } + @include e(tr, $m: pinned-top) { @extend %igx-grid__tr--pinned-top !optional; } diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index 25f0fc3addf..c992c2b95cc 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1341,6 +1341,10 @@ } %igx-grid__tr--merged { + border-bottom: 0px; + } + + %igx-grid__tr--merged-top { position: absolute; width: 100%; } diff --git a/projects/igniteui-angular/src/lib/grids/common/pipes.ts b/projects/igniteui-angular/src/lib/grids/common/pipes.ts index 27d0667fbeb..23bc4d4a9b0 100644 --- a/projects/igniteui-angular/src/lib/grids/common/pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/common/pipes.ts @@ -128,6 +128,7 @@ export class IgxGridRowClassesPipe implements PipeTransform { [deleted, 'igx-grid__tr--deleted'], [dragging, 'igx-grid__tr--drag'], [mrl || merged, 'igx-grid__tr--mrl'], + [merged, 'igx-grid__tr--merged'], // Tree grid only [filteredOut, 'igx-grid__tr--filtered'] ]; 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 830d3a95c06..77a3b166b91 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -50,7 +50,7 @@ @for (rowData of mergedDataInView; track rowData.record;) { - 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 adca8662b94..4e07604a4f3 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 @@ -33,7 +33,7 @@ } @for (rowData of mergedDataInView; track rowData.record;) { - 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 c3ecb711690..1c665795621 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 @@ -32,7 +32,7 @@ } @for (rowData of mergedDataInView; track rowData.record;) { - From 8e45f72fb456474d3b4925027e2e7f496d1bc28b Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 7 Aug 2025 15:38:29 +0300 Subject: [PATCH 081/106] chore(*): Apply review comments. Co-authored-by: Simeon Simeonoff --- .../src/lib/core/styles/components/grid/_grid-theme.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index c992c2b95cc..ffa36f14e90 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1341,7 +1341,7 @@ } %igx-grid__tr--merged { - border-bottom: 0px; + border-block-end: 0; } %igx-grid__tr--merged-top { From d6b9e8ea18368732263fd90a44c2a6927993e403 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 7 Aug 2025 16:13:30 +0300 Subject: [PATCH 082/106] chore(*): Update tests since border is now removed. --- .../src/lib/grids/grid/cell-merge.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts index 963ddd7726c..9ac6d33f653 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -164,8 +164,8 @@ describe('IgxGrid - Cell merging #grid', () => { grid.verticalScrollContainer.recalcUpdateSizes(); grid.dataRowList.toArray().forEach(x => x.cdr.detectChanges()); const mergedCell = fix.debugElement.queryAll(By.css(MERGE_CELL_CSS_CLASS))[0].nativeNode; - // one row is 100px, other is 200, 4px border - expect(mergedCell.getBoundingClientRect().height).toBe(100 + 200 + 4); + // one row is 100px, other is 200, 2px border + expect(mergedCell.getBoundingClientRect().height).toBe(100 + 200 + 2); }); }); }); @@ -220,7 +220,7 @@ describe('IgxGrid - Cell merging #grid', () => { it('horizontal virtualization should not be affected by vertically merged cells.', async() => { let mergedCell = grid.rowList.first.cells.find(x => x.column.field === 'ProductName'); expect(mergedCell.value).toBe('Ignite UI for JavaScript'); - expect(mergedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("51px 51px"); + expect(mergedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("50px 50px"); // scroll horizontally grid.navigateTo(0, 4); @@ -238,7 +238,7 @@ describe('IgxGrid - Cell merging #grid', () => { mergedCell = grid.rowList.first.cells.find(x => x.column.field === 'ProductName'); expect(mergedCell.value).toBe('Ignite UI for JavaScript'); - expect(mergedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("51px 51px"); + expect(mergedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("50px 50px"); }); }); @@ -356,7 +356,7 @@ describe('IgxGrid - Cell merging #grid', () => { const mergedCell = grid.rowList.first.cells.find(x => x.column.field === 'ProductName'); expect(mergedCell.value).toBe('Ignite UI for JavaScript'); - expect(mergedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("51px 51px"); + expect(mergedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("50px 50px"); }); }); @@ -374,7 +374,7 @@ describe('IgxGrid - Cell merging #grid', () => { expect(pinnedRow.metaData.cellMergeMeta.get(col.field)?.rowSpan).toBe(2); const mergedPinnedCell = pinnedRow.cells.find(x => x.column.field === 'ProductName'); expect(mergedPinnedCell.value).toBe('Ignite UI for JavaScript'); - expect(mergedPinnedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("51px 51px"); + expect(mergedPinnedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("50px 50px"); }); it('should merge adjacent ghost rows in unpinned area.', () => { @@ -391,7 +391,7 @@ describe('IgxGrid - Cell merging #grid', () => { expect(ghostRow.metaData.cellMergeMeta.get(col.field)?.rowSpan).toBe(2); const mergedPinnedCell = ghostRow.cells.find(x => x.column.field === 'ProductName'); expect(mergedPinnedCell.value).toBe('Ignite UI for JavaScript'); - expect(mergedPinnedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("51px 51px"); + expect(mergedPinnedCell.nativeElement.parentElement.style.gridTemplateRows).toBe("50px 50px"); }); it('should not merge ghost and data rows together.', () => { From e91b0971aef1c02eb965b39d4e756947d5192f33 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 14 Aug 2025 16:05:14 +0300 Subject: [PATCH 083/106] chore(*): Add explicit notifyChange after activeNode is changed. --- projects/igniteui-angular/src/lib/grids/grid-base.directive.ts | 1 + 1 file changed, 1 insertion(+) 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 8930a292755..a71b6275f44 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3914,6 +3914,7 @@ export abstract class IgxGridBaseDirective implements GridType, this._activeRowIndexes = null; if (this.hasCellsToMerge) { this.refreshSearch(); + this.notifyChanges(); } }); From 6e103acbbb64c9aa1ff0c2d69df662b4a9329c80 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 14 Aug 2025 16:20:38 +0300 Subject: [PATCH 084/106] chore(*): Add merge strategy interface api docs. --- .../src/lib/data-operations/merge-strategy.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index d1e5b5f9b5a..f50e83feecc 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -6,19 +6,34 @@ import { GridType } from '../grids/common/grid.interface'; export interface IMergeByResult { rowSpan: number; root?: any; - prev?: any; } +/** + * Merge strategy interface. + */ export interface IGridMergeStrategy { /* blazorSuppress */ + /** + * Function that processes merging of the whole data per merged field. + * Returns collection where object has reference to the original record and map of the cell merge metadata per field. + */ merge: ( + /* The original data to merge. */ data: any[], + /* The field in the data to merge. */ field: string, + /* Custom comparer function to use for field. */ comparer: (prevRecord: any, currentRecord: any, field: string) => boolean, + /* Existing merge result to which to add the field specific metadata for merging. */ result: any[], - activeRowIndexes : number[], + /* The active row indexes, where merging should break the sequence. */ + activeRowIndexes: number[], + /* Optional reference to the grid */ grid?: GridType ) => any[]; + /** + * Function that compares values for merging. Returns true if same, false if different. + */ comparer: (prevRecord: any, record: any, field: string) => boolean; } @@ -35,7 +50,7 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { field: string, comparer: (prevRecord: any, record: any, field: string) => boolean = this.comparer, result: any[], - activeRowIndexes : number[], + activeRowIndexes: number[], grid?: GridType ) { let prev = null; @@ -45,7 +60,7 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { const recData = result[index]; // if this is active row or some special record type - add and skip merging if (activeRowIndexes.indexOf(index) != -1 || (grid && grid.isDetailRecord(rec) || grid.isGroupByRecord(rec) || grid.isChildGridRecord(rec))) { - if(!recData) { + if (!recData) { result.push(rec); } prev = null; From 75fd965e6590fac0fa1b5fb656e890f59159453f Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 14 Aug 2025 16:25:58 +0300 Subject: [PATCH 085/106] chore(*): Small review comments. --- .../igniteui-angular/src/lib/grids/grid-base.directive.ts | 7 +------ .../src/lib/grids/grid/grid-row.component.html | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) 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 a71b6275f44..f8f179e5d3f 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -4083,12 +4083,7 @@ export abstract class IgxGridBaseDirective implements GridType, } protected get hasCellsToMerge() { - const columnToMerge = this.visibleColumns.filter( - x => x.merge && (this.cellMergeMode ==='always' || - (this.cellMergeMode === 'onSort' && !!this.sortingExpressions - .find(y => y.fieldName === x.field))) - ); - return columnToMerge.length > 0; + return this.columnsToMerge.length > 0; } /** diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html index 75f3df188dc..0180b72cee9 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html @@ -171,7 +171,6 @@ Date: Thu, 14 Aug 2025 18:06:31 +0300 Subject: [PATCH 086/106] chore(*): Limit how often visibleColumns array changes. --- projects/igniteui-angular/src/lib/core/utils.ts | 11 +++++++++++ .../src/lib/grids/grid-base.directive.ts | 10 ++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/src/lib/core/utils.ts b/projects/igniteui-angular/src/lib/core/utils.ts index a787a9cdcd5..902d58e76d0 100644 --- a/projects/igniteui-angular/src/lib/core/utils.ts +++ b/projects/igniteui-angular/src/lib/core/utils.ts @@ -33,6 +33,17 @@ export function cloneArray(array: T[], deep = false): T[] { return deep ? (array ?? []).map(cloneValue) : (array ?? []).slice(); } +/** + * @hidden + */ +export function areEqualArrays(arr1: T[], arr2: T[]): boolean { + if (arr1.length !== arr2.length) return false; + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) return false; + } + return true; +} + /** * Doesn't clone leaf items * 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 f8f179e5d3f..68b648d5ce2 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -33,7 +33,7 @@ import { ViewContainerRef, DOCUMENT } from '@angular/core'; -import { columnFieldPath, formatDate, resizeObservable } from '../core/utils'; +import { areEqualArrays, columnFieldPath, formatDate, resizeObservable } from '../core/utils'; import { IgcTrialWatermark } from 'igniteui-trial-watermark'; import { Subject, pipe, fromEvent, animationFrameScheduler, merge } from 'rxjs'; import { takeUntil, first, filter, throttleTime, map, shareReplay, takeWhile } from 'rxjs/operators'; @@ -3358,6 +3358,7 @@ export abstract class IgxGridBaseDirective implements GridType, private _defaultRowHeight = 50; private _rowCount: number; private _cellMergeMode: GridCellMergeMode = GridCellMergeMode.onSort; + private _prevVisibleColumns: IgxColumnComponent[] = []; /** * @hidden @internal @@ -4733,7 +4734,12 @@ export abstract class IgxGridBaseDirective implements GridType, if (this._visibleColumns.length) { return this._visibleColumns; } - this._visibleColumns = this._columns.filter(c => !c.hidden); + const newCollection = this._columns.filter(c => !c.hidden); + if (areEqualArrays(newCollection, this._prevVisibleColumns)) { + return this._prevVisibleColumns; + } + this._visibleColumns = newCollection; + this._prevVisibleColumns = [...this._visibleColumns]; return this._visibleColumns; } From 1a4fbd9a56e3cfdaf466ea72eaa3b4e61a3e3c2a Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 15 Aug 2025 11:24:18 +0300 Subject: [PATCH 087/106] chore(*): Cache columnsToMerge and use as pipe trigger. --- .../src/lib/grids/grid-base.directive.ts | 22 ++++++++++++------- .../src/lib/grids/grid/grid.component.html | 4 ++-- .../src/lib/grids/grid/grid.pipes.ts | 3 +-- .../hierarchical-grid.component.html | 4 ++-- .../grids/tree-grid/tree-grid.component.html | 4 ++-- 5 files changed, 21 insertions(+), 16 deletions(-) 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 68b648d5ce2..71199ea711e 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3358,7 +3358,8 @@ export abstract class IgxGridBaseDirective implements GridType, private _defaultRowHeight = 50; private _rowCount: number; private _cellMergeMode: GridCellMergeMode = GridCellMergeMode.onSort; - private _prevVisibleColumns: IgxColumnComponent[] = []; + private _prevColsToMerge: IgxColumnComponent[] = []; + private _columnsToMerge: IgxColumnComponent[] = []; /** * @hidden @internal @@ -3994,10 +3995,19 @@ export abstract class IgxGridBaseDirective implements GridType, * @internal */ public get columnsToMerge() : ColumnType[] { - return this.visibleColumns.filter( + if (this._columnsToMerge.length) { + return this._columnsToMerge; + } + const cols = this.visibleColumns.filter( x => x.merge && (this.cellMergeMode ==='always' || (this.cellMergeMode === 'onSort' && !!this.sortingExpressions.find( y => y.fieldName === x.field))) ); + if (areEqualArrays(cols, this._prevColsToMerge)) { + return this._prevColsToMerge; + } + this._columnsToMerge = cols; + this._prevColsToMerge = [...cols]; + return this._columnsToMerge; } protected get mergedDataInView() { @@ -4015,6 +4025,7 @@ export abstract class IgxGridBaseDirective implements GridType, this._visibleColumns.length = 0; this._pinnedVisible.length = 0; this._unpinnedVisible.length = 0; + this._columnsToMerge.length = 0; } /** @@ -4734,12 +4745,7 @@ export abstract class IgxGridBaseDirective implements GridType, if (this._visibleColumns.length) { return this._visibleColumns; } - const newCollection = this._columns.filter(c => !c.hidden); - if (areEqualArrays(newCollection, this._prevVisibleColumns)) { - return this._prevVisibleColumns; - } - this._visibleColumns = newCollection; - this._prevVisibleColumns = [...this._visibleColumns]; + this._visibleColumns = this._columns.filter(c => !c.hidden); return this._visibleColumns; } 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 a27794ce4c7..8b2202aa031 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -62,7 +62,7 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:groupingExpressions:sortStrategy:id:pipeTrigger:true - | gridCellMerge:visibleColumns:cellMergeMode:mergeStrategy:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData) { + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData) { @if (pinnedData.length > 0) {
0) {
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 1c665795621..dd57a128572 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 @@ -47,7 +47,7 @@ | gridRowPinning:id:true:pipeTrigger | treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true - | gridCellMerge:visibleColumns:cellMergeMode:mergeStrategy:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
From 048327355ddf2783762d96ab49f5e67bd2d458c4 Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 15 Aug 2025 11:35:12 +0300 Subject: [PATCH 088/106] chore(*): Remove sortExpr as pipe trigger, since no longer needed. --- .../igniteui-angular/src/lib/grids/grid/grid.component.html | 4 ++-- projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 2 +- .../grids/hierarchical-grid/hierarchical-grid.component.html | 4 ++-- .../src/lib/grids/tree-grid/tree-grid.component.html | 4 ++-- src/app/grid-cellMerging/grid-cellMerging.component.html | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) 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 8b2202aa031..2569e614de8 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -62,7 +62,7 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:groupingExpressions:sortStrategy:id:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData) { + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData) { @if (pinnedData.length > 0) {
0) {
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 dd57a128572..0c9a7f12a9a 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 @@ -47,7 +47,7 @@ | gridRowPinning:id:true:pipeTrigger | treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:sortingExpressions:activeRowIndexes:true:pipeTrigger; as pinnedData + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 29b78e55884..a0b8501c44d 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -44,7 +44,7 @@

Grid with cell merge

+ [cellMergeMode]="'onSort'" [rowSelection]="'single'"> Value: {{val}},Index: {{cell.row.index}} From 6d148a498faceea03d0893c6111fa865d8ce08b1 Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 15 Aug 2025 14:52:59 +0300 Subject: [PATCH 089/106] chore(*): More optimizations for merged cols eval. --- .../src/lib/grids/columns/column.component.ts | 8 ++++- .../src/lib/grids/grid-base.directive.ts | 30 ++++++++++++++----- .../grid-cellMerging.component.html | 2 +- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts index e091a03dcf6..1e044edf6a6 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column.component.ts @@ -124,7 +124,13 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy console.warn('Merging is not supported with multi-row layouts.'); return; } - this._merge = value; + if (value !== this._merge) { + this._merge = value; + if (this.grid) { + this.grid.resetColumnCollections(); + this.grid.notifyChanges(); + } + } } /** 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 71199ea711e..70b7e533d59 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -2828,6 +2828,9 @@ export abstract class IgxGridBaseDirective implements GridType, public set sortingExpressions(value: ISortingExpression[]) { this._sortingExpressions = cloneArray(value); this.sortingExpressionsChange.emit(this._sortingExpressions); + if (this.cellMergeMode === GridCellMergeMode.onSort) { + this.resetColumnCollections(); + } this.notifyChanges(); } @@ -2951,7 +2954,11 @@ export abstract class IgxGridBaseDirective implements GridType, } public set cellMergeMode(value: GridCellMergeMode) { - this._cellMergeMode = value; + if (value !== this._cellMergeMode) { + this._cellMergeMode = value; + this.resetColumnCollections(); + this.notifyChanges(); + } } /** @@ -3358,7 +3365,6 @@ export abstract class IgxGridBaseDirective implements GridType, private _defaultRowHeight = 50; private _rowCount: number; private _cellMergeMode: GridCellMergeMode = GridCellMergeMode.onSort; - private _prevColsToMerge: IgxColumnComponent[] = []; private _columnsToMerge: IgxColumnComponent[] = []; /** @@ -4002,14 +4008,22 @@ export abstract class IgxGridBaseDirective implements GridType, x => x.merge && (this.cellMergeMode ==='always' || (this.cellMergeMode === 'onSort' && !!this.sortingExpressions.find( y => y.fieldName === x.field))) ); - if (areEqualArrays(cols, this._prevColsToMerge)) { - return this._prevColsToMerge; - } this._columnsToMerge = cols; - this._prevColsToMerge = [...cols]; return this._columnsToMerge; } + protected allowResetOfColumnsToMerge() { + const cols = this.visibleColumns.filter( + x => x.merge && (this.cellMergeMode ==='always' || + (this.cellMergeMode === 'onSort' && !!this.sortingExpressions.find( y => y.fieldName === x.field))) + ); + if (areEqualArrays(cols, this._columnsToMerge)) { + return false; + } else { + return true + } + } + protected get mergedDataInView() { return this._mergedDataInView; } @@ -4025,7 +4039,9 @@ export abstract class IgxGridBaseDirective implements GridType, this._visibleColumns.length = 0; this._pinnedVisible.length = 0; this._unpinnedVisible.length = 0; - this._columnsToMerge.length = 0; + if (this.allowResetOfColumnsToMerge()) { + this._columnsToMerge.length = 0; + } } /** diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index a0b8501c44d..29b78e55884 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -44,7 +44,7 @@

Grid with cell merge

+ [cellMergeMode]="'always'" [rowSelection]="'single'"> Value: {{val}},Index: {{cell.row.index}} From a17df96e68d89fac4b6fc0057402ce674f190648 Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 18 Aug 2025 14:09:07 +0300 Subject: [PATCH 090/106] chore(*): Remove change detect on mergeStrategy change. --- projects/igniteui-angular/src/lib/grids/grid-base.directive.ts | 3 --- 1 file changed, 3 deletions(-) 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 70b7e533d59..5e840192f1a 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -2519,9 +2519,6 @@ export abstract class IgxGridBaseDirective implements GridType, } public set mergeStrategy(value) { this._mergeStrategy = value; - if (!this._init) { - this.cdr.detectChanges(); - } } /** From a892a578d4d59d0c2f78df0b47bc7743b40bc62a Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 18 Aug 2025 14:59:48 +0300 Subject: [PATCH 091/106] chore(*): Add handling for different date related dataTypes on column. --- .../src/lib/data-operations/data-util.ts | 16 ++++- .../src/lib/data-operations/merge-strategy.ts | 66 +++++++++++++++---- .../src/lib/grids/grid/cell-merge.spec.ts | 2 + .../grid-cellMerging.component.html | 2 +- 4 files changed, 70 insertions(+), 16 deletions(-) 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 0066cd58f4e..52a594c085a 100644 --- a/projects/igniteui-angular/src/lib/data-operations/data-util.ts +++ b/projects/igniteui-angular/src/lib/data-operations/data-util.ts @@ -25,7 +25,7 @@ import { DefaultMergeStrategy, IGridMergeStrategy } from './merge-strategy'; /** * @hidden */ - export const DataType = { +export const DataType = { String: 'string', Number: 'number', Boolean: 'boolean', @@ -95,10 +95,20 @@ export class DataUtil { ): any[] { let result = []; for (const col of columns) { - strategy.merge(data, col.field, col.mergingComparer, result, activeRowIndexes, grid); + const isDate = col?.dataType === 'date' || col?.dataType === 'dateTime'; + const isTime = col?.dataType === 'time' || col?.dataType === 'dateTime'; + strategy.merge( + data, + col.field, + col.mergingComparer, + result, + activeRowIndexes, + isDate, + isTime, + grid); } return result; -} + } public static page(data: T[], state: IPagingState, dataLength?: number): T[] { if (!state) { diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index f50e83feecc..726140f962f 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -1,3 +1,4 @@ +import { columnFieldPath, parseDate, resolveNestedPath } from '../core/utils'; import { GridType } from '../grids/common/grid.interface'; @@ -28,7 +29,11 @@ export interface IGridMergeStrategy { result: any[], /* The active row indexes, where merging should break the sequence. */ activeRowIndexes: number[], - /* Optional reference to the grid */ + /* (Optional) Indicates if the field is of type Date. */ + isDate?: boolean, + /* (Optional) Indicates if the field is of type Time. */ + isTime?: boolean, + /* (Optional) Reference to the grid */ grid?: GridType ) => any[]; /** @@ -48,9 +53,11 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { public merge( data: any[], field: string, - comparer: (prevRecord: any, record: any, field: string) => boolean = this.comparer, + comparer: (prevRecord: any, record: any, field: string, isDate?: boolean, isTime?: boolean) => boolean = this.comparer, result: any[], activeRowIndexes: number[], + isDate = false, + isTime = false, grid?: GridType ) { let prev = null; @@ -69,7 +76,7 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { } let recToUpdateData = recData ?? { recordRef: grid.isGhostRecord(rec) ? rec.recordRef : rec, cellMergeMeta: new Map(), ghostRecord: rec.ghostRecord }; recToUpdateData.cellMergeMeta.set(field, { rowSpan: 1 }); - if (prev && comparer(prev.recordRef, recToUpdateData.recordRef, field) && prev.ghostRecord === recToUpdateData.ghostRecord) { + if (prev && comparer.call(this, prev.recordRef, recToUpdateData.recordRef, field, isDate, isTime) && prev.ghostRecord === recToUpdateData.ghostRecord) { const root = prev.cellMergeMeta.get(field)?.root ?? prev; root.cellMergeMeta.get(field).rowSpan += 1; recToUpdateData.cellMergeMeta.get(field).root = root; @@ -84,9 +91,9 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { } /* blazorSuppress */ - public comparer(prevRecord: any, record: any, field: string): boolean { - const a = prevRecord[field]; - const b = record[field]; + public comparer(prevRecord: any, record: any, field: string, isDate = false, isTime = false): boolean { + const a = this.getFieldValue(prevRecord,field, isDate, isTime); + const b = this.getFieldValue(record,field, isDate, isTime); const an = (a === null || a === undefined); const bn = (b === null || b === undefined); if (an) { @@ -99,14 +106,49 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { } return a === b; } + + /** + * Retrieves the value of the specified field from the given object, considering date and time data types. + * `key`: The key of the field to retrieve. + * `isDate`: (Optional) Indicates if the field is of type Date. + * `isTime`: (Optional) Indicates if the field is of type Time. + * Returns the value of the specified field in the data object. + * @internal + */ + protected getFieldValue(obj: T, key: string, isDate = false, isTime = false) { + let resolvedValue = resolveNestedPath(obj, columnFieldPath(key)); + if (isDate || isTime) { + resolvedValue = this.getDateValue(resolvedValue, isDate, isTime); + } + return resolvedValue; + } + + /** + * @internal + */ + protected getDateValue(obj: T, isDate = false, isTime = false) { + let date = obj instanceof Date ? obj : parseDate(obj); + let resolvedValue; + if (isDate && isTime) { + // date + time + resolvedValue = date.getTime(); + } else if (date && isDate && !isTime) { + // date, but no time + resolvedValue = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0).getTime(); + } else if (date && isTime && !isDate) { + // just time + resolvedValue = new Date(new Date().setHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds())).getTime(); + } + return resolvedValue; + } } export class DefaultTreeGridMergeStrategy extends DefaultMergeStrategy { /* blazorSuppress */ - public override comparer(prevRecord: any, record: any, field: string): boolean { - const a = prevRecord.data[field]; - const b = record.data[field]; + public override comparer(prevRecord: any, record: any, field: string, isDate = false, isTime = false): boolean { + const a = this.getFieldValue( prevRecord.data, field, isDate, isTime); + const b = this.getFieldValue(record.data,field, isDate, isTime); const an = (a === null || a === undefined); const bn = (b === null || b === undefined); if (an) { @@ -123,9 +165,9 @@ export class DefaultTreeGridMergeStrategy extends DefaultMergeStrategy { export class ByLevelTreeGridMergeStrategy extends DefaultMergeStrategy { /* blazorSuppress */ - public override comparer(prevRecord: any, record: any, field: string): boolean { - const a = prevRecord.data[field]; - const b = record.data[field]; + public override comparer(prevRecord: any, record: any, field: string, isDate = false, isTime = false): boolean { + const a = this.getFieldValue( prevRecord.data, field, isDate, isTime); + const b = this.getFieldValue(record.data,field, isDate, isTime); const levelA = prevRecord.level; const levelB = record.level; const an = (a === null || a === undefined); diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts index 9ac6d33f653..9d7adeb7cb6 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -1021,6 +1021,8 @@ class NoopMergeStrategy extends DefaultMergeStrategy { comparer: (prevRecord: any, record: any, field: string) => boolean = this.comparer, result: any[], activeRowIndexes: number[], + isDate?: boolean, + isTime?: boolean, grid?: GridType ) { return data; diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 29b78e55884..73233eb9c1c 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -52,7 +52,7 @@

Grid with cell merge

- + From a503659ce8bc338379e89a191ee645701e03e97d Mon Sep 17 00:00:00 2001 From: MKirova Date: Mon, 18 Aug 2025 16:52:46 +0300 Subject: [PATCH 092/106] chore(*): Add test for date column. --- .../src/lib/grids/grid/cell-merge.spec.ts | 22 +++++++++++++++++++ .../src/lib/test-utils/grid-functions.spec.ts | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts index 9d7adeb7cb6..4bcf4a4ef54 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/cell-merge.spec.ts @@ -138,6 +138,28 @@ describe('IgxGrid - Cell merging #grid', () => { { value: 'Ignite UI for JavaScript', span: 9 } ]); }); + + it('should merge date column correctly.', () => { + const col = grid.getColumnByName('ReleaseDate'); + + grid.sort({ fieldName: 'ReleaseDate', dir: SortingDirection.Desc, ignoreCase: false }); + fix.detectChanges(); + + // merge date column + col.merge = true; + fix.detectChanges(); + + const today: Date = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate(), 0, 0, 0); + const nextDay = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() + 1, 0, 0, 0); + const prevDay = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() - 1, 0, 0, 0); + + GridFunctions.verifyColumnMergedState(grid, col, [ + { value: nextDay, span: 2 }, + { value: today, span: 2 }, + { value: prevDay, span: 3 }, + { value: null, span: 2 } + ]); + }); }); describe('UI', () => { diff --git a/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts index 0a757a3ef3d..483332e700c 100644 --- a/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/grid-functions.spec.ts @@ -112,8 +112,8 @@ export class GridFunctions { const cellValue = row.cells.toArray().find(x => x.column === col).value; const rowSpan = row.metaData?.cellMergeMeta.get(col.field)?.rowSpan || 1; const currState = state[index - totalSpan]; - expect(cellValue).toBe(currState.value); - expect(rowSpan).toBe(currState.span); + expect(cellValue).toEqual(currState.value); + expect(rowSpan).toEqual(currState.span); totalSpan += (rowSpan - 1); index += (rowSpan - 1); } From a786e0c56ef51f301f4ecdce7e27e24ac25812fa Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 19 Aug 2025 17:51:46 +0300 Subject: [PATCH 093/106] chore(*): Split merge handling in 2 pipes. --- .../src/lib/data-operations/merge-strategy.ts | 4 +- .../src/lib/grids/grid/grid.component.html | 6 +- .../src/lib/grids/grid/grid.component.ts | 5 +- .../src/lib/grids/grid/grid.pipes.ts | 57 ++++++++++++++++++- .../hierarchical-grid.component.html | 6 +- .../hierarchical-grid.component.ts | 5 +- .../grids/tree-grid/tree-grid.component.html | 6 +- .../grids/tree-grid/tree-grid.component.ts | 5 +- .../grid-cellMerging.component.html | 7 +-- .../grid-cellMerging.component.ts | 13 ++++- 10 files changed, 94 insertions(+), 20 deletions(-) diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index 726140f962f..7b622c69563 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -7,6 +7,7 @@ import { GridType } from '../grids/common/grid.interface'; export interface IMergeByResult { rowSpan: number; root?: any; + childRecords?: any[]; } /** @@ -75,10 +76,11 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { continue; } let recToUpdateData = recData ?? { recordRef: grid.isGhostRecord(rec) ? rec.recordRef : rec, cellMergeMeta: new Map(), ghostRecord: rec.ghostRecord }; - recToUpdateData.cellMergeMeta.set(field, { rowSpan: 1 }); + recToUpdateData.cellMergeMeta.set(field, { rowSpan: 1, childRecords: [] }); if (prev && comparer.call(this, prev.recordRef, recToUpdateData.recordRef, field, isDate, isTime) && prev.ghostRecord === recToUpdateData.ghostRecord) { const root = prev.cellMergeMeta.get(field)?.root ?? prev; root.cellMergeMeta.get(field).rowSpan += 1; + root.cellMergeMeta.get(field).childRecords.push(recToUpdateData); recToUpdateData.cellMergeMeta.get(field).root = root; } prev = recToUpdateData; 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 2569e614de8..8afe1830fba 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -62,7 +62,8 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:groupingExpressions:sortStrategy:id:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData) { + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger + | gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData) { @if (pinnedData.length > 0) {
x - this.grid.pinnedRecordsCount); } - const result = DataUtil.merge(cloneArray(collection), colsToMerge, mergeStrategy, activeRowIndexes, this.grid); + const result = cloneArray(collection) as any; + activeRowIndexes = activeRowIndexes.filter((val, idx, arr) => arr.indexOf(val) === idx).filter(x => !isNaN(x)); + const rootsToUpdate = []; + activeRowIndexes.forEach(index => { + const target = collection[index]; + colsToMerge.forEach(col => { + const colMeta = target.cellMergeMeta.get(col.field); + const root = colMeta.root || target; + rootsToUpdate.push(root); + }); + }); + + rootsToUpdate.forEach(x => { + const index = result.indexOf(x); + const colKeys = [...x.cellMergeMeta.keys()]; + const cols = colsToMerge.filter(x => colKeys.indexOf(x.field) !== -1); + for (const col of cols) { + const rs = x.cellMergeMeta.get(col.field).rowSpan; + let childData = x.cellMergeMeta.get(col.field).childRecords; + const childRecs = childData.map(x => x.recordRef); + const isDate = col?.dataType === 'date' || col?.dataType === 'dateTime'; + const isTime = col?.dataType === 'time' || col?.dataType === 'dateTime'; + const res = this.grid.mergeStrategy.merge( + [x.recordRef, ...childRecs], + col.field, + col.mergingComparer, + [], + activeRowIndexes, + isDate, + isTime, + this.grid); + result.splice(index, index + rs, ...res); + } + + }); + + return result; } } 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 2bb8d815e2a..0556ba7abec 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 @@ -45,7 +45,8 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:[]:sortStrategy:id:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger + | gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
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 c17c6d67207..a653e24fb02 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 @@ -50,7 +50,7 @@ import { IgxGridValidationService } from '../grid/grid-validation.service'; import { IgxGridHierarchicalPipe, IgxGridHierarchicalPagingPipe } from './hierarchical-grid.pipes'; import { IgxSummaryDataPipe } from '../summaries/grid-root-summary.pipe'; import { IgxGridTransactionPipe, IgxHasVisibleColumnsPipe, IgxGridRowPinningPipe, IgxGridAddRowPipe, IgxGridRowClassesPipe, IgxGridRowStylesPipe, IgxStringReplacePipe } from '../common/pipes'; -import { IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe } from '../grid/grid.pipes'; +import { IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from '../grid/grid.pipes'; import { IgxGridColumnResizerComponent } from '../resizing/resizer.component'; import { IgxRowEditTabStopDirective } from '../grid.rowEdit.directive'; import { IgxIconComponent } from '../../icon/icon.component'; @@ -351,7 +351,8 @@ export class IgxChildGridRowComponent implements AfterViewInit, OnInit { IgxGridHierarchicalPipe, IgxGridHierarchicalPagingPipe, IgxStringReplacePipe, - IgxGridCellMergePipe + IgxGridCellMergePipe, + IgxGridUnmergeActivePipe ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) 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 0c9a7f12a9a..7e52746d498 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 @@ -47,7 +47,8 @@ | gridRowPinning:id:true:pipeTrigger | treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger + | gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts index 32a424a37a7..a87ce8381e1 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts @@ -82,7 +82,7 @@ import { IgxGridDragSelectDirective } from '../selection/drag-select.directive'; import { IgxGridBodyDirective } from '../grid.common'; import { IgxGridHeaderRowComponent } from '../headers/grid-header-row.component'; import { IgxTextHighlightService } from '../../directives/text-highlight/text-highlight.service'; -import { IgxGridCellMergePipe } from '../grid/grid.pipes'; +import { IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from '../grid/grid.pipes'; import { DefaultTreeGridMergeStrategy, IGridMergeStrategy } from '../../data-operations/merge-strategy'; let NEXT_ID = 0; @@ -171,7 +171,8 @@ let NEXT_ID = 0; IgxTreeGridNormalizeRecordsPipe, IgxTreeGridAddRowPipe, IgxStringReplacePipe, - IgxGridCellMergePipe + IgxGridCellMergePipe, + IgxGridUnmergeActivePipe ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 73233eb9c1c..4bd48642348 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -50,9 +50,9 @@

Grid with cell merge

Value: {{val}},Index: {{cell.row.index}} - + - + @@ -70,8 +70,7 @@

Grid with cell merge

- - + diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index 3374c5e606f..f84a8294dd9 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -38,7 +38,7 @@ import { INVOICE_DATA } from '../shared/invoiceData'; FormsModule, IgxColumnComponent, IgxGridComponent, - IgxPaginatorComponent, + // IgxPaginatorComponent, IgxActionStripComponent, IgxGridPinningActionsComponent, IgxGridToolbarComponent, @@ -66,6 +66,17 @@ export class GridCellMergingComponent { @ViewChild('grid1', { static: true }) public grid: IgxGridComponent; public data = INVOICE_DATA; + constructor(){ + const allData = INVOICE_DATA + const length = INVOICE_DATA.length; + for (let i = 1; i <= 600_000; i++) { + const rnd = Math.floor(Math.random() * length); + allData.push(INVOICE_DATA[rnd]); + } + + this.data = allData; + } + public toggleStrategy() { if (this.treeGridMergeStrategy instanceof ByLevelTreeGridMergeStrategy) { this.treeGridMergeStrategy = new DefaultTreeGridMergeStrategy(); From 05ea472be4f08da981334f9379f110b589ea426f Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 19 Aug 2025 17:58:56 +0300 Subject: [PATCH 094/106] chore(*): Fix collection to splice into original merge data. --- .../igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 11 ++++++----- .../grid-cellMerging/grid-cellMerging.component.html | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index 6e493b7f195..53cc6839d4e 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -126,24 +126,25 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { const index = result.indexOf(x); const colKeys = [...x.cellMergeMeta.keys()]; const cols = colsToMerge.filter(x => colKeys.indexOf(x.field) !== -1); + let res = []; for (const col of cols) { - const rs = x.cellMergeMeta.get(col.field).rowSpan; + let childData = x.cellMergeMeta.get(col.field).childRecords; const childRecs = childData.map(x => x.recordRef); const isDate = col?.dataType === 'date' || col?.dataType === 'dateTime'; const isTime = col?.dataType === 'time' || col?.dataType === 'dateTime'; - const res = this.grid.mergeStrategy.merge( + res = this.grid.mergeStrategy.merge( [x.recordRef, ...childRecs], col.field, col.mergingComparer, - [], + res, activeRowIndexes, isDate, isTime, this.grid); - result.splice(index, index + rs, ...res); - } + } + result.splice(index, (index + res.length - 1), ...res); }); diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 4bd48642348..b578fb8c3ef 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -52,7 +52,7 @@

Grid with cell merge

- + From 7f0dd37dcc3c4ea51942acf9e0fad4361c41042a Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 20 Aug 2025 11:41:26 +0300 Subject: [PATCH 095/106] chore(*): Additional small fixes. --- .../igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index 53cc6839d4e..b5d2552902a 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -121,8 +121,8 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { rootsToUpdate.push(root); }); }); - - rootsToUpdate.forEach(x => { + const uniqueRoots = rootsToUpdate.filter((val, idx, arr) => arr.indexOf(val) === idx); + uniqueRoots.forEach(x => { const index = result.indexOf(x); const colKeys = [...x.cellMergeMeta.keys()]; const cols = colsToMerge.filter(x => colKeys.indexOf(x.field) !== -1); @@ -138,13 +138,13 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { col.field, col.mergingComparer, res, - activeRowIndexes, + activeRowIndexes.map(x => x - index), isDate, isTime, this.grid); } - result.splice(index, (index + res.length - 1), ...res); + result.splice(index, res.length, ...res); }); From 652ac65216d73bd5abd8cfef032a51c976bf8313 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 20 Aug 2025 12:00:06 +0300 Subject: [PATCH 096/106] chore(*): Additional small perf. improvements. --- .../src/lib/grids/grid/grid.pipes.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index b5d2552902a..e7d1c8bde8d 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -110,18 +110,24 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { if (!pinned && this.grid.isPinningToStart) { activeRowIndexes = activeRowIndexes.map(x => x - this.grid.pinnedRecordsCount); } - const result = cloneArray(collection) as any; activeRowIndexes = activeRowIndexes.filter((val, idx, arr) => arr.indexOf(val) === idx).filter(x => !isNaN(x)); const rootsToUpdate = []; activeRowIndexes.forEach(index => { const target = collection[index]; colsToMerge.forEach(col => { const colMeta = target.cellMergeMeta.get(col.field); - const root = colMeta.root || target; - rootsToUpdate.push(root); + const root = colMeta.root || (colMeta.rowSpan > 1 ? target : null); + if (root) { + rootsToUpdate.push(root); + } }); }); const uniqueRoots = rootsToUpdate.filter((val, idx, arr) => arr.indexOf(val) === idx); + if (uniqueRoots.length === 0) { + // if nothing to update, return + return collection; + } + const result = cloneArray(collection) as any; uniqueRoots.forEach(x => { const index = result.indexOf(x); const colKeys = [...x.cellMergeMeta.keys()]; From 69ba10dd37258ad73b2d332d3e23185ef4c4d05c Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 20 Aug 2025 12:51:53 +0300 Subject: [PATCH 097/106] chore(*): Apply review comments. --- .../igniteui-angular/src/lib/data-operations/merge-strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index 726140f962f..3768b84c458 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -137,7 +137,7 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { resolvedValue = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0).getTime(); } else if (date && isTime && !isDate) { // just time - resolvedValue = new Date(new Date().setHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds())).getTime(); + resolvedValue = new Date().setHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()); } return resolvedValue; } From 638df68bb4ab6d44ff23af601665499e1aa41794 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 20 Aug 2025 13:12:01 +0300 Subject: [PATCH 098/106] chore(*): Add scroll inertia in merged rows outside of virt.frame. --- .../src/lib/grids/grid/grid.component.html | 18 ++++++++++++------ .../src/lib/grids/grid/grid.component.ts | 4 +++- .../hierarchical-grid.component.html | 17 +++++++++++------ .../hierarchical-grid.component.ts | 4 +++- .../grids/tree-grid/tree-grid.component.html | 16 +++++++++++----- .../lib/grids/tree-grid/tree-grid.component.ts | 4 +++- 6 files changed, 43 insertions(+), 20 deletions(-) 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 2569e614de8..c68eb11c48d 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -47,13 +47,19 @@ - - @for (rowData of mergedDataInView; track rowData.record;) { - - + @if (mergedDataInView && mergedDataInView.length > 0) { +
+ @for (rowData of mergedDataInView; track rowData.record;) { + + + } +
} + @if (data | gridTransaction:id:pipeTrigger diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts index 7a670eff860..247975c757f 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts @@ -54,6 +54,7 @@ import { IgxGridBodyDirective } from '../grid.common'; import { IgxGridHeaderRowComponent } from '../headers/grid-header-row.component'; import { IgxGridGroupByAreaComponent } from '../grouping/grid-group-by-area.component'; import { Observable, Subject } from 'rxjs'; +import { IgxScrollInertiaDirective } from '../../directives/scroll-inertia/scroll_inertia.directive'; let NEXT_ID = 0; @@ -152,7 +153,8 @@ export interface IGroupingDoneEventArgs extends IBaseEventArgs { IgxGridSummaryPipe, IgxGridDetailsPipe, IgxStringReplacePipe, - IgxGridCellMergePipe + IgxGridCellMergePipe, + IgxScrollInertiaDirective ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) 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 2bb8d815e2a..fa977e5f472 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 @@ -30,12 +30,17 @@ [igxColumnMovingDrop]="headerContainer" [attr.droppable]="true" id="left" class="igx-grid__scroll-on-drag-pinned" [style.left.px]="pinnedWidth"> } - - @for (rowData of mergedDataInView; track rowData.record;) { - - + @if (mergedDataInView && mergedDataInView.length > 0) { +
+ @for (rowData of mergedDataInView; track rowData.record;) { + + + } +
} @if (data 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 c17c6d67207..988fa60ec80 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 @@ -68,6 +68,7 @@ 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'; +import { IgxScrollInertiaDirective } from '../../directives/scroll-inertia/scroll_inertia.directive'; let NEXT_ID = 0; @@ -351,7 +352,8 @@ export class IgxChildGridRowComponent implements AfterViewInit, OnInit { IgxGridHierarchicalPipe, IgxGridHierarchicalPagingPipe, IgxStringReplacePipe, - IgxGridCellMergePipe + IgxGridCellMergePipe, + IgxScrollInertiaDirective ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) 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 0c9a7f12a9a..7da608e583d 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 @@ -31,11 +31,17 @@ class="igx-grid__scroll-on-drag-pinned" [style.left.px]="pinnedWidth"> } - @for (rowData of mergedDataInView; track rowData.record;) { - - + @if (mergedDataInView && mergedDataInView.length > 0) { +
+ @for (rowData of mergedDataInView; track rowData.record;) { + + + } +
} diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts index 32a424a37a7..4820bca8339 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts @@ -84,6 +84,7 @@ import { IgxGridHeaderRowComponent } from '../headers/grid-header-row.component' import { IgxTextHighlightService } from '../../directives/text-highlight/text-highlight.service'; import { IgxGridCellMergePipe } from '../grid/grid.pipes'; import { DefaultTreeGridMergeStrategy, IGridMergeStrategy } from '../../data-operations/merge-strategy'; +import { IgxScrollInertiaDirective } from '../../directives/scroll-inertia/scroll_inertia.directive'; let NEXT_ID = 0; @@ -171,7 +172,8 @@ let NEXT_ID = 0; IgxTreeGridNormalizeRecordsPipe, IgxTreeGridAddRowPipe, IgxStringReplacePipe, - IgxGridCellMergePipe + IgxGridCellMergePipe, + IgxScrollInertiaDirective ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) From 050832f6ae3d0fed8e6bc2cbf45413c7c064de58 Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 26 Sep 2025 12:13:44 +0300 Subject: [PATCH 099/106] chore(*): Split merge handling in 2 pipes. --- .../src/lib/data-operations/merge-strategy.ts | 4 +- .../src/lib/grids/grid/grid.component.html | 6 +- .../src/lib/grids/grid/grid.component.ts | 5 +- .../src/lib/grids/grid/grid.pipes.ts | 57 ++++++++++++++++++- .../hierarchical-grid.component.html | 6 +- .../hierarchical-grid.component.ts | 5 +- .../grids/tree-grid/tree-grid.component.html | 6 +- .../grids/tree-grid/tree-grid.component.ts | 5 +- .../grid-cellMerging.component.html | 7 +-- .../grid-cellMerging.component.ts | 13 ++++- 10 files changed, 94 insertions(+), 20 deletions(-) diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index 9bf47444fec..0c604bb21b6 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -7,6 +7,7 @@ import { GridType } from '../grids/common/grid.interface'; export interface IMergeByResult { rowSpan: number; root?: any; + childRecords?: any[]; } /** @@ -75,10 +76,11 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { continue; } const recToUpdateData = recData ?? { recordRef: grid.isGhostRecord(rec) ? rec.recordRef : rec, cellMergeMeta: new Map(), ghostRecord: rec.ghostRecord }; - recToUpdateData.cellMergeMeta.set(field, { rowSpan: 1 }); + recToUpdateData.cellMergeMeta.set(field, { rowSpan: 1, childRecords: [] }); if (prev && comparer.call(this, prev.recordRef, recToUpdateData.recordRef, field, isDate, isTime) && prev.ghostRecord === recToUpdateData.ghostRecord) { const root = prev.cellMergeMeta.get(field)?.root ?? prev; root.cellMergeMeta.get(field).rowSpan += 1; + root.cellMergeMeta.get(field).childRecords.push(recToUpdateData); recToUpdateData.cellMergeMeta.get(field).root = root; } prev = recToUpdateData; 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 9631c1bd71a..f7b47ea5aec 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -69,7 +69,8 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:groupingExpressions:sortStrategy:id:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData) { + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger + | gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData) { @if (pinnedData.length > 0) {
x - this.grid.pinnedRecordsCount); } - const result = DataUtil.merge(cloneArray(collection), colsToMerge, mergeStrategy, activeRowIndexes, this.grid); + const result = cloneArray(collection) as any; + activeRowIndexes = activeRowIndexes.filter((val, idx, arr) => arr.indexOf(val) === idx).filter(x => !isNaN(x)); + const rootsToUpdate = []; + activeRowIndexes.forEach(index => { + const target = collection[index]; + colsToMerge.forEach(col => { + const colMeta = target.cellMergeMeta.get(col.field); + const root = colMeta.root || target; + rootsToUpdate.push(root); + }); + }); + + rootsToUpdate.forEach(x => { + const index = result.indexOf(x); + const colKeys = [...x.cellMergeMeta.keys()]; + const cols = colsToMerge.filter(x => colKeys.indexOf(x.field) !== -1); + for (const col of cols) { + const rs = x.cellMergeMeta.get(col.field).rowSpan; + let childData = x.cellMergeMeta.get(col.field).childRecords; + const childRecs = childData.map(x => x.recordRef); + const isDate = col?.dataType === 'date' || col?.dataType === 'dateTime'; + const isTime = col?.dataType === 'time' || col?.dataType === 'dateTime'; + const res = this.grid.mergeStrategy.merge( + [x.recordRef, ...childRecs], + col.field, + col.mergingComparer, + [], + activeRowIndexes, + isDate, + isTime, + this.grid); + result.splice(index, index + rs, ...res); + } + + }); + + return result; } } 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 276e89cb377..2b640e97ddd 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 @@ -51,7 +51,8 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:[]:sortStrategy:id:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger + | gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
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 80c7c7c1f96..789b46b4453 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 @@ -50,7 +50,7 @@ import { IgxGridValidationService } from '../grid/grid-validation.service'; import { IgxGridHierarchicalPipe, IgxGridHierarchicalPagingPipe } from './hierarchical-grid.pipes'; import { IgxSummaryDataPipe } from '../summaries/grid-root-summary.pipe'; import { IgxGridTransactionPipe, IgxHasVisibleColumnsPipe, IgxGridRowPinningPipe, IgxGridAddRowPipe, IgxGridRowClassesPipe, IgxGridRowStylesPipe, IgxStringReplacePipe } from '../common/pipes'; -import { IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe } from '../grid/grid.pipes'; +import { IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from '../grid/grid.pipes'; import { IgxGridColumnResizerComponent } from '../resizing/resizer.component'; import { IgxRowEditTabStopDirective } from '../grid.rowEdit.directive'; import { IgxIconComponent } from '../../icon/icon.component'; @@ -353,7 +353,8 @@ export class IgxChildGridRowComponent implements AfterViewInit, OnInit { IgxGridHierarchicalPagingPipe, IgxStringReplacePipe, IgxGridCellMergePipe, - IgxScrollInertiaDirective + IgxScrollInertiaDirective, + IgxGridUnmergeActivePipe ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) 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 33c79416332..7195fcbf71b 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 @@ -53,7 +53,8 @@ | gridRowPinning:id:true:pipeTrigger | treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger + | gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts index 4820bca8339..40196a62d0b 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts @@ -82,7 +82,7 @@ import { IgxGridDragSelectDirective } from '../selection/drag-select.directive'; import { IgxGridBodyDirective } from '../grid.common'; import { IgxGridHeaderRowComponent } from '../headers/grid-header-row.component'; import { IgxTextHighlightService } from '../../directives/text-highlight/text-highlight.service'; -import { IgxGridCellMergePipe } from '../grid/grid.pipes'; +import { IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from '../grid/grid.pipes'; import { DefaultTreeGridMergeStrategy, IGridMergeStrategy } from '../../data-operations/merge-strategy'; import { IgxScrollInertiaDirective } from '../../directives/scroll-inertia/scroll_inertia.directive'; @@ -173,7 +173,8 @@ let NEXT_ID = 0; IgxTreeGridAddRowPipe, IgxStringReplacePipe, IgxGridCellMergePipe, - IgxScrollInertiaDirective + IgxScrollInertiaDirective, + IgxGridUnmergeActivePipe ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 73233eb9c1c..4bd48642348 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -50,9 +50,9 @@

Grid with cell merge

Value: {{val}},Index: {{cell.row.index}} - + - + @@ -70,8 +70,7 @@

Grid with cell merge

- - + diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index 3374c5e606f..f84a8294dd9 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -38,7 +38,7 @@ import { INVOICE_DATA } from '../shared/invoiceData'; FormsModule, IgxColumnComponent, IgxGridComponent, - IgxPaginatorComponent, + // IgxPaginatorComponent, IgxActionStripComponent, IgxGridPinningActionsComponent, IgxGridToolbarComponent, @@ -66,6 +66,17 @@ export class GridCellMergingComponent { @ViewChild('grid1', { static: true }) public grid: IgxGridComponent; public data = INVOICE_DATA; + constructor(){ + const allData = INVOICE_DATA + const length = INVOICE_DATA.length; + for (let i = 1; i <= 600_000; i++) { + const rnd = Math.floor(Math.random() * length); + allData.push(INVOICE_DATA[rnd]); + } + + this.data = allData; + } + public toggleStrategy() { if (this.treeGridMergeStrategy instanceof ByLevelTreeGridMergeStrategy) { this.treeGridMergeStrategy = new DefaultTreeGridMergeStrategy(); From a6e7a47271c5edc7f2568ecdabaa191b9e6b5984 Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 19 Aug 2025 17:58:56 +0300 Subject: [PATCH 100/106] chore(*): Fix collection to splice into original merge data. --- .../igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 11 ++++++----- .../grid-cellMerging/grid-cellMerging.component.html | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index 7fd398b847d..e680d04cf47 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -126,24 +126,25 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { const index = result.indexOf(x); const colKeys = [...x.cellMergeMeta.keys()]; const cols = colsToMerge.filter(x => colKeys.indexOf(x.field) !== -1); + let res = []; for (const col of cols) { - const rs = x.cellMergeMeta.get(col.field).rowSpan; + let childData = x.cellMergeMeta.get(col.field).childRecords; const childRecs = childData.map(x => x.recordRef); const isDate = col?.dataType === 'date' || col?.dataType === 'dateTime'; const isTime = col?.dataType === 'time' || col?.dataType === 'dateTime'; - const res = this.grid.mergeStrategy.merge( + res = this.grid.mergeStrategy.merge( [x.recordRef, ...childRecs], col.field, col.mergingComparer, - [], + res, activeRowIndexes, isDate, isTime, this.grid); - result.splice(index, index + rs, ...res); - } + } + result.splice(index, (index + res.length - 1), ...res); }); diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 4bd48642348..b578fb8c3ef 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -52,7 +52,7 @@

Grid with cell merge

- + From f6db66fe79988faf545d7a65c98d76ce310c0cee Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 20 Aug 2025 11:41:26 +0300 Subject: [PATCH 101/106] chore(*): Additional small fixes. --- .../igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index e680d04cf47..cda90c2bd01 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -121,8 +121,8 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { rootsToUpdate.push(root); }); }); - - rootsToUpdate.forEach(x => { + const uniqueRoots = rootsToUpdate.filter((val, idx, arr) => arr.indexOf(val) === idx); + uniqueRoots.forEach(x => { const index = result.indexOf(x); const colKeys = [...x.cellMergeMeta.keys()]; const cols = colsToMerge.filter(x => colKeys.indexOf(x.field) !== -1); @@ -138,13 +138,13 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { col.field, col.mergingComparer, res, - activeRowIndexes, + activeRowIndexes.map(x => x - index), isDate, isTime, this.grid); } - result.splice(index, (index + res.length - 1), ...res); + result.splice(index, res.length, ...res); }); From c5404ce604af9118e202c41f1d10beb7fb13a5dd Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 20 Aug 2025 12:00:06 +0300 Subject: [PATCH 102/106] chore(*): Additional small perf. improvements. --- .../src/lib/grids/grid/grid.pipes.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index cda90c2bd01..44887c241d5 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -110,18 +110,24 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { if (this.grid.hasPinnedRecords && !pinned && this.grid.pinning.rows !== RowPinningPosition.Bottom) { activeRowIndexes = activeRowIndexes.map(x => x - this.grid.pinnedRecordsCount); } - const result = cloneArray(collection) as any; activeRowIndexes = activeRowIndexes.filter((val, idx, arr) => arr.indexOf(val) === idx).filter(x => !isNaN(x)); const rootsToUpdate = []; activeRowIndexes.forEach(index => { const target = collection[index]; colsToMerge.forEach(col => { const colMeta = target.cellMergeMeta.get(col.field); - const root = colMeta.root || target; - rootsToUpdate.push(root); + const root = colMeta.root || (colMeta.rowSpan > 1 ? target : null); + if (root) { + rootsToUpdate.push(root); + } }); }); const uniqueRoots = rootsToUpdate.filter((val, idx, arr) => arr.indexOf(val) === idx); + if (uniqueRoots.length === 0) { + // if nothing to update, return + return collection; + } + const result = cloneArray(collection) as any; uniqueRoots.forEach(x => { const index = result.indexOf(x); const colKeys = [...x.cellMergeMeta.keys()]; From fe346183fe6e67d55df7e8d1854e5a4f106a0926 Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 26 Sep 2025 12:27:35 +0300 Subject: [PATCH 103/106] chore(*): Split merge handling in 2 pipes. --- .../src/lib/data-operations/merge-strategy.ts | 2 + .../src/lib/grids/grid/grid.component.html | 6 +- .../src/lib/grids/grid/grid.component.ts | 3 +- .../src/lib/grids/grid/grid.pipes.ts | 57 ++++++++++++++++++- .../hierarchical-grid.component.html | 6 +- .../hierarchical-grid.component.ts | 5 +- .../grids/tree-grid/tree-grid.component.html | 6 +- .../grids/tree-grid/tree-grid.component.ts | 5 +- .../grid-cellMerging.component.html | 7 +-- .../grid-cellMerging.component.ts | 13 ++++- 10 files changed, 92 insertions(+), 18 deletions(-) diff --git a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts index 9bf47444fec..70f2451ebcb 100644 --- a/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts @@ -7,6 +7,7 @@ import { GridType } from '../grids/common/grid.interface'; export interface IMergeByResult { rowSpan: number; root?: any; + childRecords?: any[]; } /** @@ -79,6 +80,7 @@ export class DefaultMergeStrategy implements IGridMergeStrategy { if (prev && comparer.call(this, prev.recordRef, recToUpdateData.recordRef, field, isDate, isTime) && prev.ghostRecord === recToUpdateData.ghostRecord) { const root = prev.cellMergeMeta.get(field)?.root ?? prev; root.cellMergeMeta.get(field).rowSpan += 1; + root.cellMergeMeta.get(field).childRecords.push(recToUpdateData); recToUpdateData.cellMergeMeta.get(field).root = root; } prev = recToUpdateData; 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 9631c1bd71a..f7b47ea5aec 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -69,7 +69,8 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:groupingExpressions:sortStrategy:id:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData) { + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger + | gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData) { @if (pinnedData.length > 0) {
x - this.grid.pinnedRecordsCount); } - const result = DataUtil.merge(cloneArray(collection), colsToMerge, mergeStrategy, activeRowIndexes, this.grid); + const result = cloneArray(collection) as any; + activeRowIndexes = activeRowIndexes.filter((val, idx, arr) => arr.indexOf(val) === idx).filter(x => !isNaN(x)); + const rootsToUpdate = []; + activeRowIndexes.forEach(index => { + const target = collection[index]; + colsToMerge.forEach(col => { + const colMeta = target.cellMergeMeta.get(col.field); + const root = colMeta.root || target; + rootsToUpdate.push(root); + }); + }); + + rootsToUpdate.forEach(x => { + const index = result.indexOf(x); + const colKeys = [...x.cellMergeMeta.keys()]; + const cols = colsToMerge.filter(x => colKeys.indexOf(x.field) !== -1); + for (const col of cols) { + const rs = x.cellMergeMeta.get(col.field).rowSpan; + let childData = x.cellMergeMeta.get(col.field).childRecords; + const childRecs = childData.map(x => x.recordRef); + const isDate = col?.dataType === 'date' || col?.dataType === 'dateTime'; + const isTime = col?.dataType === 'time' || col?.dataType === 'dateTime'; + const res = this.grid.mergeStrategy.merge( + [x.recordRef, ...childRecs], + col.field, + col.mergingComparer, + [], + activeRowIndexes, + isDate, + isTime, + this.grid); + result.splice(index, index + rs, ...res); + } + + }); + + return result; } } 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 276e89cb377..2b640e97ddd 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 @@ -51,7 +51,8 @@ | gridRowPinning:id:true:pipeTrigger | gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true | gridSort:sortingExpressions:[]:sortStrategy:id:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger + | gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
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 80c7c7c1f96..789b46b4453 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 @@ -50,7 +50,7 @@ import { IgxGridValidationService } from '../grid/grid-validation.service'; import { IgxGridHierarchicalPipe, IgxGridHierarchicalPagingPipe } from './hierarchical-grid.pipes'; import { IgxSummaryDataPipe } from '../summaries/grid-root-summary.pipe'; import { IgxGridTransactionPipe, IgxHasVisibleColumnsPipe, IgxGridRowPinningPipe, IgxGridAddRowPipe, IgxGridRowClassesPipe, IgxGridRowStylesPipe, IgxStringReplacePipe } from '../common/pipes'; -import { IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe } from '../grid/grid.pipes'; +import { IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from '../grid/grid.pipes'; import { IgxGridColumnResizerComponent } from '../resizing/resizer.component'; import { IgxRowEditTabStopDirective } from '../grid.rowEdit.directive'; import { IgxIconComponent } from '../../icon/icon.component'; @@ -353,7 +353,8 @@ export class IgxChildGridRowComponent implements AfterViewInit, OnInit { IgxGridHierarchicalPagingPipe, IgxStringReplacePipe, IgxGridCellMergePipe, - IgxScrollInertiaDirective + IgxScrollInertiaDirective, + IgxGridUnmergeActivePipe ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) 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 33c79416332..7195fcbf71b 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 @@ -53,7 +53,8 @@ | gridRowPinning:id:true:pipeTrigger | treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true | treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true - | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData + | gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger + | gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData ) { @if (pinnedData.length > 0) {
diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts index 4820bca8339..40196a62d0b 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts @@ -82,7 +82,7 @@ import { IgxGridDragSelectDirective } from '../selection/drag-select.directive'; import { IgxGridBodyDirective } from '../grid.common'; import { IgxGridHeaderRowComponent } from '../headers/grid-header-row.component'; import { IgxTextHighlightService } from '../../directives/text-highlight/text-highlight.service'; -import { IgxGridCellMergePipe } from '../grid/grid.pipes'; +import { IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from '../grid/grid.pipes'; import { DefaultTreeGridMergeStrategy, IGridMergeStrategy } from '../../data-operations/merge-strategy'; import { IgxScrollInertiaDirective } from '../../directives/scroll-inertia/scroll_inertia.directive'; @@ -173,7 +173,8 @@ let NEXT_ID = 0; IgxTreeGridAddRowPipe, IgxStringReplacePipe, IgxGridCellMergePipe, - IgxScrollInertiaDirective + IgxScrollInertiaDirective, + IgxGridUnmergeActivePipe ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 73233eb9c1c..4bd48642348 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -50,9 +50,9 @@

Grid with cell merge

Value: {{val}},Index: {{cell.row.index}} - + - + @@ -70,8 +70,7 @@

Grid with cell merge

- - + diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts index 3374c5e606f..f84a8294dd9 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.ts +++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts @@ -38,7 +38,7 @@ import { INVOICE_DATA } from '../shared/invoiceData'; FormsModule, IgxColumnComponent, IgxGridComponent, - IgxPaginatorComponent, + // IgxPaginatorComponent, IgxActionStripComponent, IgxGridPinningActionsComponent, IgxGridToolbarComponent, @@ -66,6 +66,17 @@ export class GridCellMergingComponent { @ViewChild('grid1', { static: true }) public grid: IgxGridComponent; public data = INVOICE_DATA; + constructor(){ + const allData = INVOICE_DATA + const length = INVOICE_DATA.length; + for (let i = 1; i <= 600_000; i++) { + const rnd = Math.floor(Math.random() * length); + allData.push(INVOICE_DATA[rnd]); + } + + this.data = allData; + } + public toggleStrategy() { if (this.treeGridMergeStrategy instanceof ByLevelTreeGridMergeStrategy) { this.treeGridMergeStrategy = new DefaultTreeGridMergeStrategy(); From b6bbf14002451eaf232e3c7f8b6f6b7ddd904aba Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 19 Aug 2025 17:58:56 +0300 Subject: [PATCH 104/106] chore(*): Fix collection to splice into original merge data. --- .../igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 11 ++++++----- .../grid-cellMerging/grid-cellMerging.component.html | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index 7fd398b847d..e680d04cf47 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -126,24 +126,25 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { const index = result.indexOf(x); const colKeys = [...x.cellMergeMeta.keys()]; const cols = colsToMerge.filter(x => colKeys.indexOf(x.field) !== -1); + let res = []; for (const col of cols) { - const rs = x.cellMergeMeta.get(col.field).rowSpan; + let childData = x.cellMergeMeta.get(col.field).childRecords; const childRecs = childData.map(x => x.recordRef); const isDate = col?.dataType === 'date' || col?.dataType === 'dateTime'; const isTime = col?.dataType === 'time' || col?.dataType === 'dateTime'; - const res = this.grid.mergeStrategy.merge( + res = this.grid.mergeStrategy.merge( [x.recordRef, ...childRecs], col.field, col.mergingComparer, - [], + res, activeRowIndexes, isDate, isTime, this.grid); - result.splice(index, index + rs, ...res); - } + } + result.splice(index, (index + res.length - 1), ...res); }); diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html index 4bd48642348..b578fb8c3ef 100644 --- a/src/app/grid-cellMerging/grid-cellMerging.component.html +++ b/src/app/grid-cellMerging/grid-cellMerging.component.html @@ -52,7 +52,7 @@

Grid with cell merge

- + From bab82c6c8ae1c3dc463f8bff46bdaf9ac004f803 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 20 Aug 2025 11:41:26 +0300 Subject: [PATCH 105/106] chore(*): Additional small fixes. --- .../igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index e680d04cf47..cda90c2bd01 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -121,8 +121,8 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { rootsToUpdate.push(root); }); }); - - rootsToUpdate.forEach(x => { + const uniqueRoots = rootsToUpdate.filter((val, idx, arr) => arr.indexOf(val) === idx); + uniqueRoots.forEach(x => { const index = result.indexOf(x); const colKeys = [...x.cellMergeMeta.keys()]; const cols = colsToMerge.filter(x => colKeys.indexOf(x.field) !== -1); @@ -138,13 +138,13 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { col.field, col.mergingComparer, res, - activeRowIndexes, + activeRowIndexes.map(x => x - index), isDate, isTime, this.grid); } - result.splice(index, (index + res.length - 1), ...res); + result.splice(index, res.length, ...res); }); From c60ab0dec6203038fca05a69930de2dbb42c2250 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 20 Aug 2025 12:00:06 +0300 Subject: [PATCH 106/106] chore(*): Additional small perf. improvements. --- .../src/lib/grids/grid/grid.pipes.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts index cda90c2bd01..44887c241d5 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -110,18 +110,24 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { if (this.grid.hasPinnedRecords && !pinned && this.grid.pinning.rows !== RowPinningPosition.Bottom) { activeRowIndexes = activeRowIndexes.map(x => x - this.grid.pinnedRecordsCount); } - const result = cloneArray(collection) as any; activeRowIndexes = activeRowIndexes.filter((val, idx, arr) => arr.indexOf(val) === idx).filter(x => !isNaN(x)); const rootsToUpdate = []; activeRowIndexes.forEach(index => { const target = collection[index]; colsToMerge.forEach(col => { const colMeta = target.cellMergeMeta.get(col.field); - const root = colMeta.root || target; - rootsToUpdate.push(root); + const root = colMeta.root || (colMeta.rowSpan > 1 ? target : null); + if (root) { + rootsToUpdate.push(root); + } }); }); const uniqueRoots = rootsToUpdate.filter((val, idx, arr) => arr.indexOf(val) === idx); + if (uniqueRoots.length === 0) { + // if nothing to update, return + return collection; + } + const result = cloneArray(collection) as any; uniqueRoots.forEach(x => { const index = result.indexOf(x); const colKeys = [...x.cellMergeMeta.keys()];