Skip to content

Commit 8694d53

Browse files
authored
Hierarchical grid: Export proper child data on inner levels to PDF (#16594)
1 parent 1dbe82e commit 8694d53

File tree

2 files changed

+107
-54
lines changed

2 files changed

+107
-54
lines changed

projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import { GridIDNameJobTitleComponent } from '../../../../../test-utils/grid-samp
66
import { first } from 'rxjs/operators';
77
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
88
import { NestedColumnGroupsGridComponent, ColumnGroupTestComponent, BlueWhaleGridComponent } from '../../../../../test-utils/grid-mch-sample.spec';
9-
import { IgxHierarchicalGridTestBaseComponent } from '../../../../../test-utils/hierarchical-grid-components.spec';
9+
import { IgxHierarchicalGridExportComponent, IgxHierarchicalGridTestBaseComponent } from '../../../../../test-utils/hierarchical-grid-components.spec';
1010
import { IgxTreeGridSortingComponent, IgxTreeGridPrimaryForeignKeyComponent } from '../../../../../test-utils/tree-grid-components.spec';
1111
import { CustomSummariesComponent } from 'igniteui-angular/grids/grid/src/grid-summary.spec';
1212
import { IgxHierarchicalGridComponent } from 'igniteui-angular/grids/hierarchical-grid';
1313
import { IgxPivotGridMultipleRowComponent, IgxPivotGridTestComplexHierarchyComponent } from '../../../../../test-utils/pivot-grid-samples.spec';
1414
import { IgxPivotGridComponent } from 'igniteui-angular/grids/pivot-grid';
1515
import { PivotRowLayoutType } from 'igniteui-angular/grids/core';
16-
import { wait } from 'igniteui-angular/test-utils/ui-interactions.spec';
16+
import { UIInteractions, wait } from 'igniteui-angular/test-utils/ui-interactions.spec';
1717

1818
describe('PDF Grid Exporter', () => {
1919
let exporter: IgxPdfExporterService;
@@ -328,6 +328,50 @@ describe('PDF Grid Exporter', () => {
328328
exporter.export(grid, options);
329329
});
330330

331+
it('should export the correct number of child data rows from a hierarchical grid', (done) => {
332+
const fix = TestBed.createComponent(IgxHierarchicalGridExportComponent);
333+
fix.detectChanges();
334+
335+
const hGrid = fix.componentInstance.hGrid;
336+
hGrid.data = hGrid.data.slice(0, 1); // Limit data for test performance
337+
338+
// Expand first few rows to realize all inner levels, same as in Excel tests
339+
const firstRow = hGrid.gridAPI.get_row_by_index(0) as any;
340+
341+
UIInteractions.simulateClickAndSelectEvent(firstRow.expander);
342+
fix.detectChanges();
343+
344+
let childGrids = hGrid.gridAPI.getChildGrids(false) as any[];
345+
const firstChildGrid = childGrids[0];
346+
const firstChildRow = firstChildGrid.gridAPI.get_row_by_index(0) as any;
347+
const secondChildRow = firstChildGrid.gridAPI.get_row_by_index(1) as any;
348+
UIInteractions.simulateClickAndSelectEvent(secondChildRow.expander);
349+
fix.detectChanges();
350+
351+
UIInteractions.simulateClickAndSelectEvent(firstChildRow.expander);
352+
fix.detectChanges();
353+
354+
childGrids = hGrid.gridAPI.getChildGrids(false) as any[];
355+
const thirdChildGrid = childGrids[1];
356+
const thirdChildRow = thirdChildGrid.gridAPI.get_row_by_index(0) as any;
357+
UIInteractions.simulateClickAndSelectEvent(thirdChildRow.expander);
358+
fix.detectChanges();
359+
360+
// Calculate expected number of data rows to be exported
361+
const allGrids = [hGrid, ...(hGrid.gridAPI.getChildGrids(true) as any[])];
362+
const expectedRows = allGrids.reduce((acc, g) => acc + g.data.length, 0);
363+
364+
// Spy PDF row drawing to count exported rows
365+
const drawDataRowSpy = spyOn<any>(exporter as any, 'drawDataRow').and.callThrough();
366+
367+
exporter.exportEnded.pipe(first()).subscribe(() => {
368+
expect(drawDataRowSpy.calls.count()).toBe(expectedRows);
369+
done();
370+
});
371+
372+
exporter.export(hGrid, options);
373+
});
374+
331375
it('should export tree grid with hierarchical data', (done) => {
332376
TestBed.configureTestingModule({
333377
imports: [

projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.ts

Lines changed: 61 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -352,28 +352,21 @@ export class IgxPdfExporterService extends IgxBaseExporter {
352352

353353
// For hierarchical grids, check if this record has child records
354354
if (recordIsHierarchicalGrid) {
355-
const allDescendants = [];
355+
const allDescendants: Array<IExportRecord & { __index: number }> = [];
356356

357-
// Collect all descendant records (children, grandchildren, etc.) that belong to this parent
358-
// Child records have a different owner (island object) than the parent
359357
let j = i + 1;
360358
while (j < data.length && data[j].level > record.level) {
361-
// Include all descendants (any level deeper)
362359
if (!data[j].hidden) {
363-
allDescendants.push(data[j]);
360+
// Attach the original index into data
361+
allDescendants.push({ ...(data[j] as any), __index: j });
364362
}
365363
j++;
366364
}
367365

368-
// If there are descendant records, draw child table(s)
369366
if (allDescendants.length > 0) {
370-
// Group descendants by owner to separate different child grids
371-
// Owner is the actual island object, not a string
372-
// Only collect DIRECT children (one level deeper) for initial grouping
373-
const directDescendantsByOwner = new Map<any, IExportRecord[]>();
367+
const directDescendantsByOwner = new Map<any, Array<IExportRecord & { __index: number }>>();
374368

375369
for (const desc of allDescendants) {
376-
// Only include records that are exactly one level deeper (direct children)
377370
if (desc.level === record.level + 1) {
378371
const owner = desc.owner;
379372
if (!directDescendantsByOwner.has(owner)) {
@@ -383,13 +376,12 @@ export class IgxPdfExporterService extends IgxBaseExporter {
383376
}
384377
}
385378

386-
// Draw each child grid separately with its direct children only
387379
for (const [owner, directChildren] of directDescendantsByOwner) {
388380
yPosition = this.drawHierarchicalChildren(
389381
pdf,
390382
data,
391-
allDescendants, // Pass all descendants so grandchildren can be found
392-
directChildren,
383+
allDescendants, // descendants WITH __index
384+
directChildren, // direct children WITH __index
393385
owner,
394386
yPosition,
395387
margin,
@@ -402,7 +394,6 @@ export class IgxPdfExporterService extends IgxBaseExporter {
402394
);
403395
}
404396

405-
// Skip the descendant records we just processed
406397
i = j - 1;
407398
}
408399
}
@@ -649,7 +640,7 @@ export class IgxPdfExporterService extends IgxBaseExporter {
649640
private drawHierarchicalChildren(
650641
pdf: jsPDF,
651642
allData: IExportRecord[],
652-
allDescendants: IExportRecord[], // All descendants to search for grandchildren
643+
allDescendants: any[], // All descendants to search for grandchildren
653644
childRecords: IExportRecord[], // Direct children to render at this level
654645
childOwner: any, // Owner is the island object, not a string
655646
yPosition: number,
@@ -773,49 +764,67 @@ export class IgxPdfExporterService extends IgxBaseExporter {
773764
this.drawDataRow(pdf, childRecord, childColumns, [], childTableX, yPosition, childColumnWidth, rowHeight, 0, options);
774765
yPosition += rowHeight;
775766

776-
// Check if this child has grandchildren (deeper levels in different child grids)
777-
// Look for grandchildren in allDescendants that are direct descendants of this childRecord
778-
const grandchildrenForThisRecord = allDescendants.filter(r =>
779-
r.level === childRecord.level + 1 && r.type !== 'HeaderRecord'
780-
);
767+
// allDescendants here is an array of records with an extra __index property
768+
const childIndex = (childRecord as any).__index as number | undefined;
769+
770+
if (childIndex !== undefined) {
771+
// Find this child's position in allDescendants (by original index)
772+
const childPosInDesc = allDescendants.findIndex(d => d.__index === childIndex);
781773

782-
if (grandchildrenForThisRecord.length > 0) {
783-
// Group grandchildren by their owner (different child islands under this record)
784-
const grandchildrenByOwner = new Map<any, IExportRecord[]>();
785-
786-
for (const gc of grandchildrenForThisRecord) {
787-
// Use the actual owner object
788-
const gcOwner = gc.owner;
789-
// Only include grandchildren that have a different owner (separate child grid)
790-
if (gcOwner !== childOwner) {
791-
if (!grandchildrenByOwner.has(gcOwner)) {
792-
grandchildrenByOwner.set(gcOwner, []);
774+
if (childPosInDesc !== -1) {
775+
const subtree: Array<IExportRecord & { __index: number }> = [];
776+
const childLevel = childRecord.level;
777+
778+
// Collect all deeper records until we hit same-or-higher level
779+
for (let k = childPosInDesc + 1; k < allDescendants.length; k++) {
780+
const rec = allDescendants[k];
781+
if (rec.level <= childLevel) {
782+
break;
783+
}
784+
if (rec.type !== 'HeaderRecord') {
785+
subtree.push(rec);
793786
}
794-
grandchildrenByOwner.get(gcOwner)!.push(gc);
795787
}
796-
}
797788

798-
// Recursively draw each grandchild owner's records with increased indentation
799-
for (const [gcOwner, directGrandchildren] of grandchildrenByOwner) {
800-
yPosition = this.drawHierarchicalChildren(
801-
pdf,
802-
allData,
803-
allDescendants, // Pass all descendants so great-grandchildren can be found
804-
directGrandchildren, // Direct grandchildren to render
805-
gcOwner,
806-
yPosition,
807-
margin,
808-
indentPerLevel + 20, // Increase indentation for next level
809-
usableWidth,
810-
pageHeight,
811-
headerHeight,
812-
rowHeight,
813-
options
814-
);
789+
if (subtree.length > 0) {
790+
// Direct grandchildren for this child: exactly one level deeper
791+
const grandchildrenForThisRecord = subtree.filter(r =>
792+
r.level === childRecord.level + 1 && r.owner !== childOwner
793+
);
794+
795+
if (grandchildrenForThisRecord.length > 0) {
796+
const grandchildrenByOwner = new Map<any, Array<IExportRecord & { __index: number }>>();
797+
798+
for (const gc of grandchildrenForThisRecord) {
799+
const gcOwner = gc.owner;
800+
if (!grandchildrenByOwner.has(gcOwner)) {
801+
grandchildrenByOwner.set(gcOwner, []);
802+
}
803+
grandchildrenByOwner.get(gcOwner)!.push(gc);
804+
}
805+
806+
for (const [gcOwner, directGrandchildren] of grandchildrenByOwner) {
807+
yPosition = this.drawHierarchicalChildren(
808+
pdf,
809+
allData,
810+
subtree, // only this child's subtree for deeper levels
811+
directGrandchildren,
812+
gcOwner,
813+
yPosition,
814+
margin,
815+
indentPerLevel + 20,
816+
usableWidth,
817+
pageHeight,
818+
headerHeight,
819+
rowHeight,
820+
options
821+
);
822+
}
823+
}
824+
}
815825
}
816826
}
817827
}
818-
819828
// Add spacing after child table
820829
yPosition += 5;
821830

0 commit comments

Comments
 (0)