Skip to content

Commit ca24a26

Browse files
authored
fix(csv): export summaries (#16748)
1 parent 194fb76 commit ca24a26

3 files changed

Lines changed: 72 additions & 13 deletions

File tree

projects/igniteui-angular/src/lib/services/csv/char-separated-value-data.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ export class CharSeparatedValueData {
4242
return this._headerRecord + this._dataRecords;
4343
}
4444

45-
public prepareDataAsync(done: (result: string) => void) {
45+
public prepareDataAsync(done: (result: string) => void, alwaysExportHeaders: boolean = true) {
4646
const columns = this.columns?.filter(c => !c.skip)
4747
.sort((a, b) => a.startIndex - b.startIndex)
4848
.sort((a, b) => a.pinnedIndex - b.pinnedIndex);
4949
const keys = columns && columns.length ? columns.map(c => c.field) : ExportUtilities.getKeysFromData(this._data);
5050

51-
this._isSpecialData = ExportUtilities.isSpecialData(this._data[0]);
51+
if (this._data && this._data.length > 0) {
52+
this._isSpecialData = ExportUtilities.isSpecialData(this._data[0]);
53+
}
5254
this._escapeCharacters.push(this._delimiter);
5355

5456
const headers = columns && columns.length ?
@@ -57,7 +59,12 @@ export class CharSeparatedValueData {
5759

5860
this._headerRecord = this.processHeaderRecord(headers, this._data.length);
5961
if (keys.length === 0 || ((!this._data || this._data.length === 0) && keys.length === 0)) {
60-
done('');
62+
// If alwaysExportHeaders is true and we have headers, export headers only
63+
if (alwaysExportHeaders && headers && headers.length > 0) {
64+
done(this._headerRecord);
65+
} else {
66+
done('');
67+
}
6168
} else {
6269
this.processDataRecordsAsync(this._data, keys, (dr) => {
6370
done(this._headerRecord + dr);

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { ReorderedColumnsComponent,
1212
GridIDNameJobTitleComponent,
1313
ProductsComponent,
1414
ColumnsAddedOnInitComponent,
15-
EmptyGridComponent } from '../../test-utils/grid-samples.spec';
15+
EmptyGridComponent,
16+
GridCustomSummaryComponent } from '../../test-utils/grid-samples.spec';
1617
import { SampleTestData } from '../../test-utils/sample-test-data.spec';
1718
import { first } from 'rxjs/operators';
1819
import { DefaultSortingStrategy, SortingDirection } from '../../data-operations/sorting-strategy';
@@ -40,7 +41,8 @@ describe('CSV Grid Exporter', () => {
4041
IgxTreeGridPrimaryForeignKeyComponent,
4142
ProductsComponent,
4243
ColumnsAddedOnInitComponent,
43-
EmptyGridComponent
44+
EmptyGridComponent,
45+
GridCustomSummaryComponent
4446
]
4547
}).compileComponents();
4648
}));
@@ -406,6 +408,30 @@ describe('CSV Grid Exporter', () => {
406408
wrapper.verifyData('Country,Region,Test Header', 'Only headers should be exported.');
407409
});
408410

411+
it('should export grid with summaries correctly, not as [object Object]', async () => {
412+
const fix = TestBed.createComponent(GridCustomSummaryComponent);
413+
fix.detectChanges();
414+
415+
const grid = fix.componentInstance.grid;
416+
417+
const wrapper = await getExportedData(grid, options);
418+
const exportedData = wrapper['_data'];
419+
420+
expect(exportedData.includes('[object Object]')).toBe(false, 'CSV export should not contain [object Object]');
421+
422+
const lines = exportedData.split('\r\n');
423+
424+
// Skip header line and data lines, check summary lines at the end
425+
const summaryLines = lines.slice(-4);
426+
427+
// Verify at least one summary line contains proper formatting (label: value pattern)
428+
const hasProperlySummary = summaryLines.some(line =>
429+
line.includes(':') && !line.includes('[object Object]')
430+
);
431+
432+
expect(hasProperlySummary).toBe(true, 'Summary data should be formatted as "label: value"');
433+
});
434+
409435
describe('Tree Grid CSV export', () => {
410436
let fix;
411437
let treeGrid: IgxTreeGridComponent;

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

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EventEmitter, Injectable } from '@angular/core';
2-
import { DEFAULT_OWNER, ExportHeaderType, IColumnInfo, IExportRecord, IgxBaseExporter } from '../exporter-common/base-export-service';
2+
import { DEFAULT_OWNER, ExportHeaderType, ExportRecordType, IColumnInfo, IExportRecord, IgxBaseExporter } from '../exporter-common/base-export-service';
33
import { ExportUtilities } from '../exporter-common/export-utilities';
44
import { CharSeparatedValueData } from './char-separated-value-data';
55
import { CsvFileTypes, IgxCsvExporterOptions } from './csv-exporter-options';
@@ -50,14 +50,40 @@ export class IgxCsvExporterService extends IgxBaseExporter {
5050
private _stringData: string;
5151

5252
protected exportDataImplementation(data: IExportRecord[], options: IgxCsvExporterOptions, done: () => void) {
53-
const dimensionKeys = data[0]?.dimensionKeys;
54-
data = dimensionKeys?.length ?
55-
data.map((item) => item.rawData):
56-
data.map((item) => item.data);
53+
const firstDataElement = data[0];
54+
const dimensionKeys = firstDataElement?.dimensionKeys;
55+
56+
const dataRecords = dimensionKeys?.length ?
57+
data.filter(item => item.type !== ExportRecordType.SummaryRecord).map((item) => item.rawData):
58+
data.filter(item => item.type !== ExportRecordType.SummaryRecord).map((item) => item.data);
59+
60+
// Get summary records if exportSummaries is enabled
61+
const summaryRecords: any[] = [];
62+
if (options.exportSummaries) {
63+
const summaries = data.filter(item => item.type === ExportRecordType.SummaryRecord);
64+
for (const summary of summaries) {
65+
// Convert summary record data to a flat object format for CSV
66+
const summaryData: any = {};
67+
if (summary.data) {
68+
for (const [key, value] of Object.entries(summary.data)) {
69+
if (value && typeof value === 'object' && 'label' in value && 'value' in value) {
70+
summaryData[key] = `${value.label}: ${value.value}`;
71+
} else {
72+
summaryData[key] = value;
73+
}
74+
}
75+
}
76+
summaryRecords.push(summaryData);
77+
}
78+
}
79+
80+
// Combine data records and summary records
81+
const allRecords = [...dataRecords, ...summaryRecords];
82+
5783
const columnList = this._ownersMap.get(DEFAULT_OWNER);
5884
const columns = columnList?.columns.filter(c => c.headerType === ExportHeaderType.ColumnHeader);
5985
if (dimensionKeys) {
60-
const dimensionCols = dimensionKeys.map((key) => {
86+
const dimensionCols = dimensionKeys.map((key) => {
6187
const columnInfo: IColumnInfo = {
6288
header: key,
6389
field: key,
@@ -72,13 +98,13 @@ export class IgxCsvExporterService extends IgxBaseExporter {
7298
columns.unshift(...dimensionCols);
7399
}
74100

75-
const csvData = new CharSeparatedValueData(data, options.valueDelimiter, columns);
101+
const csvData = new CharSeparatedValueData(allRecords, options.valueDelimiter, columns);
76102
csvData.prepareDataAsync((r) => {
77103
this._stringData = r;
78104
this.saveFile(options);
79105
this.exportEnded.emit({ csvData: this._stringData });
80106
done();
81-
});
107+
}, options.alwaysExportHeaders);
82108
}
83109

84110
private saveFile(options: IgxCsvExporterOptions) {

0 commit comments

Comments
 (0)