From 3b3b3f8f259c3bbb11b08b79760f1ac9bd590b4d Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 23 Oct 2025 15:30:39 +0300 Subject: [PATCH 1/7] fix(igxGrid): Fix merge when merge groups partially overlap. --- .../src/lib/grids/grid/grid.pipes.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 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 b6c2091ff0b..d2c6d0cf2c6 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -131,27 +131,17 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { } let result = cloneArray(collection) as any; uniqueRoots.forEach(x => { - const index = result.indexOf(x); + const index = collection.indexOf(x); const colKeys = [...x.cellMergeMeta.keys()]; const cols = colsToMerge.filter(col => colKeys.indexOf(col.field) !== -1); - let res = []; + let data = []; for (const col of cols) { let childData = x.cellMergeMeta.get(col.field).childRecords; const childRecs = childData.map(rec => rec.recordRef); - const isDate = col?.dataType === 'date' || col?.dataType === 'dateTime'; - const isTime = col?.dataType === 'time' || col?.dataType === 'dateTime'; - res = this.grid.mergeStrategy.merge( - [x.recordRef, ...childRecs], - col.field, - col.mergingComparer, - res, - activeRowIndexes.map(ri => ri - index), - isDate, - isTime, - this.grid); - + data = data.concat([x.recordRef, ...childRecs]); } + const res = DataUtil.merge(Array.from(new Set(data)), cols, this.grid.mergeStrategy, activeRowIndexes.map(ri => ri - index), this.grid); result = result.slice(0, index).concat(res, result.slice(index + res.length)); }); From 4fbc2dc4a0497e7b612fc776ff43e14732654789 Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 28 Oct 2025 12:24:26 +0200 Subject: [PATCH 2/7] chore(*): Fix unmerge when roots of merge groups partially overlap. --- .../src/lib/grids/grid/grid.pipes.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 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 d2c6d0cf2c6..6154a949860 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -129,24 +129,31 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { // if nothing to update, return return collection; } - let result = cloneArray(collection) as any; + + // collect full range of data to unmerge + const dataToUnmerge = new Set(); + let startIndex; uniqueRoots.forEach(x => { const index = collection.indexOf(x); + if (!startIndex) { + startIndex = index; + } else { + startIndex = Math.min(startIndex, index); + } + dataToUnmerge.add(x.recordRef); const colKeys = [...x.cellMergeMeta.keys()]; const cols = colsToMerge.filter(col => colKeys.indexOf(col.field) !== -1); - let data = []; for (const col of cols) { - - let childData = x.cellMergeMeta.get(col.field).childRecords; + const childData = x.cellMergeMeta.get(col.field).childRecords; const childRecs = childData.map(rec => rec.recordRef); - data = data.concat([x.recordRef, ...childRecs]); + childRecs.forEach(child => dataToUnmerge.add(child)); } - const res = DataUtil.merge(Array.from(new Set(data)), cols, this.grid.mergeStrategy, activeRowIndexes.map(ri => ri - index), this.grid); - result = result.slice(0, index).concat(res, result.slice(index + res.length)); }); - - return result; + // unmerge data where active row index breaks merge groups + const res = DataUtil.merge(Array.from(dataToUnmerge), colsToMerge, this.grid.mergeStrategy, activeRowIndexes.map(ri => ri - startIndex), this.grid); + collection = collection.slice(0, startIndex).concat(res, collection.slice(startIndex + res.length)); + return collection; } } From 133a43070c2550273c5f8c5efa9877f5e4023907 Mon Sep 17 00:00:00 2001 From: MKirova Date: Tue, 28 Oct 2025 13:39:51 +0200 Subject: [PATCH 3/7] chore(*): Pass full range, since same field can have multiple roots. --- .../src/lib/grids/grid/grid.pipes.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 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 6154a949860..b480ca661f9 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -131,8 +131,8 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { } // collect full range of data to unmerge - const dataToUnmerge = new Set(); let startIndex; + let endIndex; uniqueRoots.forEach(x => { const index = collection.indexOf(x); if (!startIndex) { @@ -140,18 +140,21 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { } else { startIndex = Math.min(startIndex, index); } - dataToUnmerge.add(x.recordRef); const colKeys = [...x.cellMergeMeta.keys()]; const cols = colsToMerge.filter(col => colKeys.indexOf(col.field) !== -1); for (const col of cols) { const childData = x.cellMergeMeta.get(col.field).childRecords; const childRecs = childData.map(rec => rec.recordRef); - childRecs.forEach(child => dataToUnmerge.add(child)); + if (!endIndex) { + endIndex = index + childRecs.length; + } else { + endIndex = Math.max(endIndex, index + childRecs.length + 1); + } } }); - + const dataToUnmerge = collection.slice(startIndex, endIndex).map(x => x.recordRef); // unmerge data where active row index breaks merge groups - const res = DataUtil.merge(Array.from(dataToUnmerge), colsToMerge, this.grid.mergeStrategy, activeRowIndexes.map(ri => ri - startIndex), this.grid); + const res = DataUtil.merge(dataToUnmerge, colsToMerge, this.grid.mergeStrategy, activeRowIndexes.map(ri => ri - startIndex), this.grid); collection = collection.slice(0, startIndex).concat(res, collection.slice(startIndex + res.length)); return collection; } From a407e1699e94aedf5aefe49a539af82efda33de3 Mon Sep 17 00:00:00 2001 From: MKirova Date: Wed, 29 Oct 2025 11:58:39 +0200 Subject: [PATCH 4/7] chore(*): Add test with more complex layout to unmerge on activation. --- .../src/lib/grids/grid/cell-merge.spec.ts | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) 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 c053bbce81d..44d8b53f24c 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 @@ -464,6 +464,206 @@ describe('IgxGrid - Cell merging #grid', () => { ]); }); + it('should interrupt merge sequence correctly when there are multiple overlapping merge groups affected.', async () => { + const col1 = grid.getColumnByName('ProductName'); + const col2 = grid.getColumnByName('Downloads'); + const col3 = grid.getColumnByName('Released'); + const col4 = grid.getColumnByName('ReleaseDate'); + + col1.merge = true; + col2.merge = true; + col3.merge = true; + col4.merge = true; + + fix.detectChanges(); + + const data = [ + { + Downloads: 1000, + ID: 1, + ProductName: 'Ignite UI for JavaScript', + ReleaseDate: fix.componentInstance.today, + Released: true + }, + { + Downloads: 1000, + ID: 2, + ProductName: 'Ignite UI for JavaScript', + ReleaseDate: fix.componentInstance.today, + Released: true + }, + { + Downloads: 1000, + ID: 3, + ProductName: 'Ignite UI for Angular', + ReleaseDate: fix.componentInstance.today, + Released: true + }, + { + Downloads: 1000, + ID: 4, + ProductName: 'Ignite UI for JavaScript', + ReleaseDate: fix.componentInstance.prevDay, + Released: true + }, + { + Downloads: 100, + ID: 5, + ProductName: 'Ignite UI for Angular', + ReleaseDate: fix.componentInstance.prevDay, + Released: true + }, + { + Downloads: 1000, + ID: 6, + ProductName: 'Ignite UI for Angular', + ReleaseDate: null, + Released: true + }, + { + Downloads: 0, + ID: 7, + ProductName: null, + ReleaseDate: fix.componentInstance.prevDay, + Released: true + }, + { + Downloads: 1000, + ID: 8, + ProductName: 'NetAdvantage', + ReleaseDate: fix.componentInstance.prevDay, + Released: true + }, + { + Downloads: 1000, + ID: 9, + ProductName: 'NetAdvantage', + ReleaseDate: null, + Released: true + } + ]; + fix.componentInstance.data = data; + fix.detectChanges(); + + const row1 = grid.rowList.toArray()[0]; + UIInteractions.simulateClickAndSelectEvent(row1.cells.toArray()[1].nativeElement); + await wait(1); + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col1, [ + { 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 } + ]); + + GridFunctions.verifyColumnMergedState(grid, col2, [ + { value: 1000, span: 1 }, + { value: 1000, span: 3 }, + { value: 100, span: 1 }, + { value: 1000, span: 1 }, + { value: 0, span: 1 }, + { value: 1000, span: 2 } + ]); + + GridFunctions.verifyColumnMergedState(grid, col3, [ + { value: true, span: 1 }, + { value: true, span: 8 } + ]); + + GridFunctions.verifyColumnMergedState(grid, col4, [ + { value: fix.componentInstance.today, span: 1 }, + { value: fix.componentInstance.today, span: 2 }, + { value: fix.componentInstance.prevDay, span: 2 }, + { value: null, span: 1 }, + { value: fix.componentInstance.prevDay, span: 2 }, + { value: null, span: 1 } + ]); + + const row2 = grid.rowList.toArray()[1]; + UIInteractions.simulateClickAndSelectEvent(row2.cells.toArray()[1].nativeElement); + await wait(1); + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col1, [ + { 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 } + ]); + + GridFunctions.verifyColumnMergedState(grid, col2, [ + { value: 1000, span: 1 }, + { value: 1000, span: 1 }, + { value: 1000, span: 2 }, + { value: 100, span: 1 }, + { value: 1000, span: 1 }, + { value: 0, span: 1 }, + { value: 1000, span: 2 } + ]); + + GridFunctions.verifyColumnMergedState(grid, col3, [ + { value: true, span: 1 }, + { value: true, span: 1 }, + { value: true, span: 7 } + ]); + + GridFunctions.verifyColumnMergedState(grid, col4, [ + { value: fix.componentInstance.today, span: 1 }, + { value: fix.componentInstance.today, span: 1 }, + { value: fix.componentInstance.today, span: 1 }, + { value: fix.componentInstance.prevDay, span: 2 }, + { value: null, span: 1 }, + { value: fix.componentInstance.prevDay, span: 2 }, + { value: null, span: 1 } + ]); + + const row3 = grid.rowList.toArray()[2]; + UIInteractions.simulateClickAndSelectEvent(row3.cells.toArray()[1].nativeElement); + await wait(1); + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(grid, col1, [ + { 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.verifyColumnMergedState(grid, col2, [ + { value: 1000, span: 2 }, + { value: 1000, span: 1 }, + { value: 1000, span: 1 }, + { value: 100, span: 1 }, + { value: 1000, span: 1 }, + { value: 0, span: 1 }, + { value: 1000, span: 2 } + ]); + + GridFunctions.verifyColumnMergedState(grid, col3, [ + { value: true, span: 2 }, + { value: true, span: 1 }, + { value: true, span: 6 } + ]); + + GridFunctions.verifyColumnMergedState(grid, col4, [ + { value: fix.componentInstance.today, span: 2 }, + { value: fix.componentInstance.today, span: 1 }, + { value: fix.componentInstance.prevDay, span: 2 }, + { value: null, span: 1 }, + { value: fix.componentInstance.prevDay, span: 2 }, + { value: null, span: 1 } + ]); + }); + }); describe('Updating', () => { From e90385ebba4d9836318bab004b6ff40cce058321 Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 30 Oct 2025 11:40:50 +0200 Subject: [PATCH 5/7] fix(igxGrid): Update just the unmerged recs and metadata per unmerged group. --- .../src/lib/grids/grid/grid.pipes.ts | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 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 b480ca661f9..383788200a5 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -10,7 +10,7 @@ import { FilterUtil, IFilteringStrategy } from '../../data-operations/filtering- import { ISortingExpression } from '../../data-operations/sorting-strategy'; import { IGridSortingStrategy, IGridGroupingStrategy } from '../common/strategy'; import { GridCellMergeMode, RowPinningPosition } from '../common/enums'; -import { IGridMergeStrategy } from '../../data-operations/merge-strategy'; +import { IGridMergeStrategy, IMergeByResult } from '../../data-operations/merge-strategy'; /** * @hidden @@ -130,33 +130,44 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { return collection; } - // collect full range of data to unmerge - let startIndex; - let endIndex; + let result = cloneArray(collection) as any; uniqueRoots.forEach(x => { const index = collection.indexOf(x); - if (!startIndex) { - startIndex = index; - } else { - startIndex = Math.min(startIndex, index); - } const colKeys = [...x.cellMergeMeta.keys()]; const cols = colsToMerge.filter(col => colKeys.indexOf(col.field) !== -1); for (const col of cols) { const childData = x.cellMergeMeta.get(col.field).childRecords; const childRecs = childData.map(rec => rec.recordRef); - if (!endIndex) { - endIndex = index + childRecs.length; - } else { - endIndex = Math.max(endIndex, index + childRecs.length + 1); + if(childRecs.length === 0) { + // nothing to unmerge + continue; + } + const unmergedData = DataUtil.merge([x.recordRef, ...childRecs], [col], this.grid.mergeStrategy, activeRowIndexes.map(ri => ri - index), this.grid); + for (let i = 0; i < unmergedData.length; i++) { + const unmergedRec = unmergedData[i]; + const origRecord = result[index + i]; + if (unmergedRec.cellMergeMeta?.get(col.field)) { + // deep clone of object, since we don't want to pollute the original fully merged collection. + const objCopy = { + recordRef: origRecord.recordRef, + ghostRecord: origRecord.ghostRecord, + cellMergeMeta: new Map() + }; + // deep clone of inner map + for (const [key, value] of origRecord.cellMergeMeta) { + objCopy.cellMergeMeta.set(key, structuredClone(value)); + } + // update copy with new meta from unmerged data record, but just for this column + objCopy.cellMergeMeta?.set(col.field, unmergedRec.cellMergeMeta.get(col.field)); + result[index + i] = objCopy; + } else { + // this is the unmerged record, with no merge metadata + result[index + i] = unmergedRec; + } } } }); - const dataToUnmerge = collection.slice(startIndex, endIndex).map(x => x.recordRef); - // unmerge data where active row index breaks merge groups - const res = DataUtil.merge(dataToUnmerge, colsToMerge, this.grid.mergeStrategy, activeRowIndexes.map(ri => ri - startIndex), this.grid); - collection = collection.slice(0, startIndex).concat(res, collection.slice(startIndex + res.length)); - return collection; + return result; } } From af0716915653020504000ed21d11faa40d4e189d Mon Sep 17 00:00:00 2001 From: MKirova Date: Thu, 30 Oct 2025 16:50:43 +0200 Subject: [PATCH 6/7] chore(*): Apply review comments. --- .../igniteui-angular/src/lib/grids/grid/grid.pipes.ts | 8 ++------ 1 file changed, 2 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 383788200a5..a35d1959795 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts @@ -147,16 +147,12 @@ export class IgxGridUnmergeActivePipe implements PipeTransform { const unmergedRec = unmergedData[i]; const origRecord = result[index + i]; if (unmergedRec.cellMergeMeta?.get(col.field)) { - // deep clone of object, since we don't want to pollute the original fully merged collection. + // clone of object, since we don't want to pollute the original fully merged collection. const objCopy = { recordRef: origRecord.recordRef, ghostRecord: origRecord.ghostRecord, - cellMergeMeta: new Map() + cellMergeMeta: new Map(origRecord.cellMergeMeta.entries()) }; - // deep clone of inner map - for (const [key, value] of origRecord.cellMergeMeta) { - objCopy.cellMergeMeta.set(key, structuredClone(value)); - } // update copy with new meta from unmerged data record, but just for this column objCopy.cellMergeMeta?.set(col.field, unmergedRec.cellMergeMeta.get(col.field)); result[index + i] = objCopy; From d27416e64aad4b3a01622f917c7de098ff8badf8 Mon Sep 17 00:00:00 2001 From: MKirova Date: Fri, 31 Oct 2025 15:10:15 +0200 Subject: [PATCH 7/7] chore(*): Fix flicker in tests due to async update on activeNodeChange. --- .../src/lib/grids/grid/cell-merge.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) 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 44d8b53f24c..a4db6c1c020 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 @@ -451,8 +451,10 @@ describe('IgxGrid - Cell merging #grid', () => { UIInteractions.simulateClickAndSelectEvent(row1.cells.toArray()[1].nativeElement); await wait(1); + (grid as any)._activeRowIndexes = null; fix.detectChanges(); + expect((grid as any).activeRowIndexes).toEqual([0, 0]); GridFunctions.verifyColumnMergedState(grid, col, [ { value: 'Ignite UI for JavaScript', span: 1 }, { value: 'Ignite UI for JavaScript', span: 1 }, @@ -548,8 +550,10 @@ describe('IgxGrid - Cell merging #grid', () => { const row1 = grid.rowList.toArray()[0]; UIInteractions.simulateClickAndSelectEvent(row1.cells.toArray()[1].nativeElement); await wait(1); + (grid as any)._activeRowIndexes = null; fix.detectChanges(); + expect((grid as any).activeRowIndexes).toEqual([0, 0]); GridFunctions.verifyColumnMergedState(grid, col1, [ { value: 'Ignite UI for JavaScript', span: 1 }, { value: 'Ignite UI for JavaScript', span: 1 }, @@ -586,8 +590,10 @@ describe('IgxGrid - Cell merging #grid', () => { const row2 = grid.rowList.toArray()[1]; UIInteractions.simulateClickAndSelectEvent(row2.cells.toArray()[1].nativeElement); await wait(1); + (grid as any)._activeRowIndexes = null; fix.detectChanges(); + expect((grid as any).activeRowIndexes).toEqual([1, 1]); GridFunctions.verifyColumnMergedState(grid, col1, [ { value: 'Ignite UI for JavaScript', span: 1 }, { value: 'Ignite UI for JavaScript', span: 1 }, @@ -627,8 +633,10 @@ describe('IgxGrid - Cell merging #grid', () => { const row3 = grid.rowList.toArray()[2]; UIInteractions.simulateClickAndSelectEvent(row3.cells.toArray()[1].nativeElement); await wait(1); + (grid as any)._activeRowIndexes = null; fix.detectChanges(); + expect((grid as any).activeRowIndexes).toEqual([2, 2]); GridFunctions.verifyColumnMergedState(grid, col1, [ { value: 'Ignite UI for JavaScript', span: 2 }, { value: 'Ignite UI for Angular', span: 1 },