Skip to content

Commit 15bc04b

Browse files
igdmdimitrovdkamburovonlyexeptionChronosSF
authored
feat(pivot-grid): export to excel (#12130)
* feat(pivot-grid): add pivot grid to prepare data * feat(pivot-grid-export): fixed owners map and data * feat(excel-exporter): prepare pivot grid columns correctly * feat(excel-exporter): add pivotGridRowHeader * feat(excel-exporter): export pivot grid with more than 1 filter * feat(excel-exporter): minor refactoring + styles added * feat(excel-exporter): export currency columns * chore(*): fix build and export when no row dim * chore(*): fix styles file expected string * chore(*): updated changelog * chore(*): push pivot grid export tests * chore(*): fixed pivot exporter tests Co-authored-by: Deyan Kamburov <[email protected]> Co-authored-by: IBarakov <[email protected]> Co-authored-by: Stamen Stoychev <[email protected]>
1 parent fa10f04 commit 15bc04b

File tree

14 files changed

+396
-67
lines changed

14 files changed

+396
-67
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ All notable changes for each version of this project will be documented in this
1919

2020
- For more information, check out the [README](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/query-builder/README.md), [specification](https://github.com/IgniteUI/igniteui-angular/wiki/Query-Builder) and [official documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/query-builder).
2121

22+
- `IgxExcelExporterService`
23+
- Added support for exporting `igxPivotGrid`.
24+
2225
### General
2326

2427
- **Breaking Changes** - The Excel exporter service `exportEnded` event has its `xlsx` argument type changed as `igniteui-angular` no longer depends on `JSZip`. Instead of providing a `JSZip` instance it is now an object describing the structure of the Excel file with property names corresponding to folders or files, folders being objects themselves that can be traversed down, while files have their contents as `Uint8Array`. The same structure is used to package as a zip file by `fflate`'s API.

projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import { IgxHierarchicalGridModule,
4141
} from '../../grids/hierarchical-grid/public_api';
4242
import { IgxHierarchicalRowComponent } from '../../grids/hierarchical-grid/hierarchical-row.component';
4343
import { GridFunctions } from '../../test-utils/grid-functions.spec';
44+
import { IgxPivotGridMultipleRowComponent, IgxPivotGridTestComplexHierarchyComponent } from '../../test-utils/pivot-grid-samples.spec';
45+
import { IgxPivotGridComponent, IgxPivotGridModule } from '../../grids/pivot-grid/public_api';
4446

4547
describe('Excel Exporter', () => {
4648
configureTestSuite();
@@ -64,9 +66,11 @@ describe('Excel Exporter', () => {
6466
IgxHierarchicalGridMultiColumnHeadersExportComponent,
6567
ColumnsAddedOnInitComponent,
6668
IgxHierarchicalGridMultiColumnHeaderIslandsExportComponent,
67-
GridWithThreeLevelsOfMultiColumnHeadersAndTwoRowsExportComponent
69+
GridWithThreeLevelsOfMultiColumnHeadersAndTwoRowsExportComponent,
70+
IgxPivotGridMultipleRowComponent,
71+
IgxPivotGridTestComplexHierarchyComponent
6872
],
69-
imports: [IgxGridModule, IgxTreeGridModule, IgxHierarchicalGridModule, NoopAnimationsModule]
73+
imports: [IgxGridModule, IgxTreeGridModule, IgxHierarchicalGridModule, IgxPivotGridModule, NoopAnimationsModule]
7074
}).compileComponents();
7175
}));
7276

@@ -1207,6 +1211,35 @@ describe('Excel Exporter', () => {
12071211
});
12081212
});
12091213

1214+
describe('', () => {
1215+
let fix;
1216+
let grid: IgxPivotGridComponent;
1217+
1218+
beforeEach(waitForAsync(() => {
1219+
options = createExportOptions('PivotGridGridExcelExport');
1220+
}));
1221+
1222+
it('should export pivot grid', async () => {
1223+
fix = TestBed.createComponent(IgxPivotGridMultipleRowComponent);
1224+
fix.detectChanges();
1225+
await wait(300);
1226+
1227+
grid = fix.componentInstance.pivotGrid;
1228+
1229+
await exportAndVerify(grid, options, actualData.exportPivotGridData, false);
1230+
});
1231+
1232+
it('should export hierarchical pivot grid', async () => {
1233+
fix = TestBed.createComponent(IgxPivotGridTestComplexHierarchyComponent);
1234+
fix.detectChanges();
1235+
await wait(300);
1236+
1237+
grid = fix.componentInstance.pivotGrid;
1238+
1239+
await exportAndVerify(grid, options, actualData.exportPivotGridHierarchicalData, false);
1240+
});
1241+
});
1242+
12101243
const getExportedData = (grid, exportOptions: IgxExcelExporterOptions) => {
12111244
const exportData = new Promise<ZipWrapper>((resolve) => {
12121245
exporter.exportEnded.pipe(first()).subscribe((value) => {

projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export class IgxExcelExporterService extends IgxBaseExporter {
7676
protected exportDataImplementation(data: IExportRecord[], options: IgxExcelExporterOptions, done: () => void): void {
7777
const firstDataElement = data[0];
7878
const isHierarchicalGrid = firstDataElement?.type === ExportRecordType.HierarchicalGridRecord;
79+
const isPivotGrid = firstDataElement?.type === ExportRecordType.PivotGridRecord;
7980

8081
let rootKeys;
8182
let columnCount;
@@ -117,7 +118,7 @@ export class IgxExcelExporterService extends IgxBaseExporter {
117118

118119
columnWidths = defaultOwner.columnWidths;
119120
indexOfLastPinnedColumn = defaultOwner.indexOfLastPinnedColumn;
120-
columnCount = columns.length;
121+
columnCount = isPivotGrid ? columns.length + this.pivotGridFilterFieldsCount : columns.length;
121122
rootKeys = columns.map(c => c.field);
122123
}
123124
} else {

projects/igniteui-angular/src/lib/services/excel/excel-files.ts

Lines changed: 121 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { WorksheetData } from './worksheet-data';
44

55
import { strToU8 } from 'fflate';
66
import { yieldingLoop } from '../../core/utils';
7-
import { HeaderType, ExportRecordType, IExportRecord, IColumnList } from '../exporter-common/base-export-service';
7+
import { HeaderType, ExportRecordType, IExportRecord, IColumnList, IColumnInfo } from '../exporter-common/base-export-service';
88

99
/**
1010
* @hidden
@@ -58,13 +58,15 @@ export class ThemeFile implements IExcelFile {
5858
export class WorksheetFile implements IExcelFile {
5959
private static MIN_WIDTH = 8.43;
6060
private maxOutlineLevel = 0;
61+
private sheetData = '';
6162
private dimension = '';
6263
private freezePane = '';
6364
private rowHeight = '';
6465

6566
private mergeCellStr = '';
6667
private mergeCellsCounter = 0;
6768
private rowIndex = 0;
69+
private pivotGridRowHeadersMap = new Map<number, string>();
6870

6971
public writeElement() {}
7072

@@ -82,74 +84,61 @@ export class WorksheetFile implements IExcelFile {
8284
}
8385

8486
private prepareDataAsync(worksheetData: WorksheetData, done: (cols: string, sheetData: string) => void) {
85-
let sheetData = '';
87+
this.sheetData = '';
8688
let cols = '';
8789
const dictionary = worksheetData.dataDictionary;
8890
this.rowIndex = 0;
8991

9092
if (worksheetData.isEmpty && (!worksheetData.options.alwaysExportHeaders || worksheetData.owner.columns.length === 0)) {
91-
sheetData += '<sheetData/>';
93+
this.sheetData += '<sheetData/>';
9294
this.dimension = 'A1';
93-
done('', sheetData);
95+
done('', this.sheetData);
9496
} else {
9597
const owner = worksheetData.owner;
9698
const isHierarchicalGrid = worksheetData.isHierarchical;
9799
const hasMultiColumnHeader = worksheetData.hasMultiColumnHeader;
100+
const hasMultiRowHeader = worksheetData.hasMultiRowHeader;
98101

99102
const hasUserSetIndex = owner.columns.some(col => col.exportIndex !== undefined);
100103

101104
const height = worksheetData.options.rowHeight;
102-
const rowStyle = isHierarchicalGrid ? ' s="3"' : '';
103105
this.rowHeight = height ? ` ht="${height}" customHeight="1"` : '';
106+
this.sheetData += `<sheetData>`;
104107

105-
sheetData += `<sheetData>`;
108+
let headersForLevel: IColumnInfo[] = [];
109+
110+
for(let i = 0; i <= owner.maxRowLevel; i++) {
111+
headersForLevel = owner.columns.filter(c => c.level === i && c.rowSpan > 0 && !c.skip)
112+
113+
this.printHeaders(worksheetData, headersForLevel, i, true);
114+
115+
this.rowIndex++;
116+
}
117+
118+
this.rowIndex = 0;
106119

107120
for (let i = 0; i <= owner.maxLevel; i++) {
108121
this.rowIndex++;
109-
sheetData += `<row r="${this.rowIndex}"${this.rowHeight}>`;
122+
const pivotGridColumns = this.pivotGridRowHeadersMap.get(this.rowIndex) ?? "";
123+
this.sheetData += `<row r="${this.rowIndex}"${this.rowHeight}>${pivotGridColumns}`;
110124

111-
const headersForLevel = hasMultiColumnHeader ?
112-
owner.columns
125+
const allowedColumns = owner.columns.filter(c => c.headerType !== HeaderType.RowHeader && c.headerType !== HeaderType.MultiRowHeader);
126+
127+
headersForLevel = hasMultiColumnHeader ?
128+
allowedColumns
113129
.filter(c => (c.level < i &&
114130
c.headerType !== HeaderType.MultiColumnHeader || c.level === i) && c.columnSpan > 0 && !c.skip)
115131
.sort((a, b) => a.startIndex - b.startIndex)
116132
.sort((a, b) => a.pinnedIndex - b.pinnedIndex) :
117133
hasUserSetIndex ?
118-
owner.columns.filter(c => !c.skip) :
119-
owner.columns.filter(c => !c.skip)
134+
allowedColumns.filter(c => !c.skip) :
135+
allowedColumns.filter(c => !c.skip)
120136
.sort((a, b) => a.startIndex - b.startIndex)
121137
.sort((a, b) => a.pinnedIndex - b.pinnedIndex);
122138

123-
let startValue = 0;
124-
125-
for (const currentCol of headersForLevel) {
126-
if (currentCol.level === i) {
127-
let columnCoordinate;
128-
columnCoordinate = ExcelStrings.getExcelColumn(startValue) + this.rowIndex;
129-
const columnValue = dictionary.saveValue(currentCol.header, true);
130-
sheetData += `<c r="${columnCoordinate}"${rowStyle} t="s"><v>${columnValue}</v></c>`;
131-
132-
if (i !== owner.maxLevel) {
133-
this.mergeCellsCounter++;
134-
this.mergeCellStr += ` <mergeCell ref="${columnCoordinate}:`;
135-
136-
if (currentCol.headerType === HeaderType.ColumnHeader) {
137-
columnCoordinate = ExcelStrings.getExcelColumn(startValue) + (owner.maxLevel + 1);
138-
} else {
139-
for (let k = 1; k < currentCol.columnSpan; k++) {
140-
columnCoordinate = ExcelStrings.getExcelColumn(startValue + k) + this.rowIndex;
141-
sheetData += `<c r="${columnCoordinate}"${rowStyle} />`;
142-
}
143-
}
144-
145-
this.mergeCellStr += `${columnCoordinate}" />`;
146-
}
147-
}
148-
149-
startValue += currentCol.columnSpan;
150-
}
139+
this.printHeaders(worksheetData, headersForLevel, i, false);
151140

152-
sheetData += `</row>`;
141+
this.sheetData += `</row>`;
153142
}
154143

155144
const multiColumnHeaderLevel = worksheetData.options.ignoreMultiColumnHeaders ? 0 : owner.maxLevel;
@@ -207,14 +196,14 @@ export class WorksheetFile implements IExcelFile {
207196
}
208197

209198
this.processDataRecordsAsync(worksheetData, (rows) => {
210-
sheetData += rows;
211-
sheetData += '</sheetData>';
199+
this.sheetData += rows;
200+
this.sheetData += '</sheetData>';
212201

213-
if (hasMultiColumnHeader && this.mergeCellsCounter > 0) {
214-
sheetData += `<mergeCells count="${this.mergeCellsCounter}">${this.mergeCellStr}</mergeCells>`;
202+
if ((hasMultiColumnHeader || hasMultiRowHeader) && this.mergeCellsCounter > 0) {
203+
this.sheetData += `<mergeCells count="${this.mergeCellsCounter}">${this.mergeCellStr}</mergeCells>`;
215204
}
216205

217-
done(cols, sheetData);
206+
done(cols, this.sheetData);
218207
});
219208
}
220209
}
@@ -237,7 +226,7 @@ export class WorksheetFile implements IExcelFile {
237226
recordHeaders = worksheetData.rootKeys;
238227
} else {
239228
recordHeaders = worksheetData.owner.columns
240-
.filter(c => c.headerType !== HeaderType.MultiColumnHeader && !c.skip)
229+
.filter(c => c.headerType !== HeaderType.MultiColumnHeader && c.headerType !== HeaderType.MultiRowHeader && c.headerType !== HeaderType.RowHeader && !c.skip)
241230
.sort((a, b) => a.startIndex-b.startIndex)
242231
.sort((a, b) => a.pinnedIndex-b.pinnedIndex)
243232
.map(c => c.field);
@@ -330,13 +319,15 @@ export class WorksheetFile implements IExcelFile {
330319
const sHidden = record.hidden ? ` hidden="1"` : '';
331320

332321
this.rowIndex++;
322+
const pivotGridColumns = this.pivotGridRowHeadersMap.get(this.rowIndex) ?? "";
323+
333324
rowData[0] =
334-
`<row r="${this.rowIndex}"${this.rowHeight}${outlineLevel}${sHidden}>`;
325+
`<row r="${this.rowIndex}"${this.rowHeight}${outlineLevel}${sHidden}>${pivotGridColumns}`;
335326

336327
const keys = worksheetData.isSpecialData ? [record.data] : headersForLevel;
337328

338329
for (let j = 0; j < keys.length; j++) {
339-
const col = j + (isHierarchicalGrid ? rowLevel : 0);
330+
const col = j + (isHierarchicalGrid ? rowLevel : worksheetData.isPivotGrid ? worksheetData.owner.maxRowLevel : 0);
340331

341332
const cellData = this.getCellData(worksheetData, i, col, keys[j]);
342333

@@ -381,6 +372,88 @@ export class WorksheetFile implements IExcelFile {
381372
return `<c r="${columnName}"${type}${format}><v>${value}</v></c>`;
382373
}
383374
}
375+
376+
private printHeaders(worksheetData: WorksheetData, headersForLevel: IColumnInfo[], i: number, isVertical: boolean) {
377+
let startValue = 0;
378+
let str = '';
379+
380+
const isHierarchicalGrid = worksheetData.isHierarchical;
381+
let rowStyle = isHierarchicalGrid ? ' s="3"' : '';
382+
const dictionary = worksheetData.dataDictionary;
383+
const owner = worksheetData.owner;
384+
const maxLevel = isVertical
385+
? owner.maxRowLevel
386+
: owner.maxLevel;
387+
388+
for (const currentCol of headersForLevel) {
389+
const spanLength = isVertical ? currentCol.rowSpan : currentCol.columnSpan;
390+
391+
if (currentCol.level === i) {
392+
let columnCoordinate;
393+
const column = isVertical
394+
? this.rowIndex
395+
: startValue + (owner.maxRowLevel ?? 0)
396+
397+
const rowCoordinate = isVertical
398+
? startValue + owner.maxLevel + 2
399+
: this.rowIndex
400+
401+
const columnValue = dictionary.saveValue(currentCol.header, true);
402+
403+
columnCoordinate = ExcelStrings.getExcelColumn(column) + rowCoordinate;
404+
rowStyle = isVertical && currentCol.rowSpan > 1 ? ' s="4"' : rowStyle;
405+
str = `<c r="${columnCoordinate}"${rowStyle} t="s"><v>${columnValue}</v></c>`;
406+
407+
if (isVertical) {
408+
if (this.pivotGridRowHeadersMap.has(rowCoordinate)) {
409+
this.pivotGridRowHeadersMap.set(rowCoordinate, this.pivotGridRowHeadersMap.get(rowCoordinate) + str)
410+
} else {
411+
this.pivotGridRowHeadersMap.set(rowCoordinate, str)
412+
}
413+
} else {
414+
this.sheetData += str;
415+
}
416+
417+
if (i !== maxLevel) {
418+
this.mergeCellsCounter++;
419+
this.mergeCellStr += ` <mergeCell ref="${columnCoordinate}:`;
420+
421+
if (currentCol.headerType === HeaderType.ColumnHeader) {
422+
const col = isVertical
423+
? maxLevel
424+
: startValue + (owner.maxRowLevel ?? 0);
425+
426+
const row = isVertical
427+
? rowCoordinate
428+
: owner.maxLevel + 1;
429+
430+
columnCoordinate = ExcelStrings.getExcelColumn(col) + row;
431+
} else {
432+
for (let k = 1; k < spanLength; k++) {
433+
const col = isVertical
434+
? column
435+
: column + k;
436+
437+
const row = isVertical
438+
? rowCoordinate + k
439+
: this.rowIndex;
440+
441+
columnCoordinate = ExcelStrings.getExcelColumn(col) + row;
442+
str = `<c r="${columnCoordinate}"${rowStyle} />`;
443+
444+
isVertical
445+
? this.pivotGridRowHeadersMap.set(row, str)
446+
: this.sheetData += str
447+
}
448+
}
449+
450+
this.mergeCellStr += `${columnCoordinate}" />`;
451+
}
452+
}
453+
454+
startValue += spanLength;
455+
}
456+
}
384457
}
385458

386459
/**

0 commit comments

Comments
 (0)