diff --git a/projects/igniteui-angular/src/lib/services/csv/char-separated-value-data.ts b/projects/igniteui-angular/src/lib/services/csv/char-separated-value-data.ts index ea3691015e8..a4a0820e0e6 100644 --- a/projects/igniteui-angular/src/lib/services/csv/char-separated-value-data.ts +++ b/projects/igniteui-angular/src/lib/services/csv/char-separated-value-data.ts @@ -42,13 +42,15 @@ export class CharSeparatedValueData { return this._headerRecord + this._dataRecords; } - public prepareDataAsync(done: (result: string) => void) { + public prepareDataAsync(done: (result: string) => void, alwaysExportHeaders: boolean = true) { const columns = this.columns?.filter(c => !c.skip) .sort((a, b) => a.startIndex - b.startIndex) .sort((a, b) => a.pinnedIndex - b.pinnedIndex); const keys = columns && columns.length ? columns.map(c => c.field) : ExportUtilities.getKeysFromData(this._data); - this._isSpecialData = ExportUtilities.isSpecialData(this._data[0]); + if (this._data && this._data.length > 0) { + this._isSpecialData = ExportUtilities.isSpecialData(this._data[0]); + } this._escapeCharacters.push(this._delimiter); const headers = columns && columns.length ? @@ -57,7 +59,12 @@ export class CharSeparatedValueData { this._headerRecord = this.processHeaderRecord(headers, this._data.length); if (keys.length === 0 || ((!this._data || this._data.length === 0) && keys.length === 0)) { - done(''); + // If alwaysExportHeaders is true and we have headers, export headers only + if (alwaysExportHeaders && headers && headers.length > 0) { + done(this._headerRecord); + } else { + done(''); + } } else { this.processDataRecordsAsync(this._data, keys, (dr) => { done(this._headerRecord + dr); diff --git a/projects/igniteui-angular/src/lib/services/csv/csv-exporter-grid.spec.ts b/projects/igniteui-angular/src/lib/services/csv/csv-exporter-grid.spec.ts index b4a7f98e930..bc40126c309 100644 --- a/projects/igniteui-angular/src/lib/services/csv/csv-exporter-grid.spec.ts +++ b/projects/igniteui-angular/src/lib/services/csv/csv-exporter-grid.spec.ts @@ -12,7 +12,8 @@ import { ReorderedColumnsComponent, GridIDNameJobTitleComponent, ProductsComponent, ColumnsAddedOnInitComponent, - EmptyGridComponent } from '../../test-utils/grid-samples.spec'; + EmptyGridComponent, + GridCustomSummaryComponent } from '../../test-utils/grid-samples.spec'; import { SampleTestData } from '../../test-utils/sample-test-data.spec'; import { first } from 'rxjs/operators'; import { DefaultSortingStrategy, SortingDirection } from '../../data-operations/sorting-strategy'; @@ -40,7 +41,8 @@ describe('CSV Grid Exporter', () => { IgxTreeGridPrimaryForeignKeyComponent, ProductsComponent, ColumnsAddedOnInitComponent, - EmptyGridComponent + EmptyGridComponent, + GridCustomSummaryComponent ] }).compileComponents(); })); @@ -406,6 +408,30 @@ describe('CSV Grid Exporter', () => { wrapper.verifyData('Country,Region,Test Header', 'Only headers should be exported.'); }); + it('should export grid with summaries correctly, not as [object Object]', async () => { + const fix = TestBed.createComponent(GridCustomSummaryComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + + const wrapper = await getExportedData(grid, options); + const exportedData = wrapper['_data']; + + expect(exportedData.includes('[object Object]')).toBe(false, 'CSV export should not contain [object Object]'); + + const lines = exportedData.split('\r\n'); + + // Skip header line and data lines, check summary lines at the end + const summaryLines = lines.slice(-4); + + // Verify at least one summary line contains proper formatting (label: value pattern) + const hasProperlySummary = summaryLines.some(line => + line.includes(':') && !line.includes('[object Object]') + ); + + expect(hasProperlySummary).toBe(true, 'Summary data should be formatted as "label: value"'); + }); + describe('Tree Grid CSV export', () => { let fix; let treeGrid: IgxTreeGridComponent; diff --git a/projects/igniteui-angular/src/lib/services/csv/csv-exporter.ts b/projects/igniteui-angular/src/lib/services/csv/csv-exporter.ts index d6a5c9596ae..73b241eff0e 100644 --- a/projects/igniteui-angular/src/lib/services/csv/csv-exporter.ts +++ b/projects/igniteui-angular/src/lib/services/csv/csv-exporter.ts @@ -1,5 +1,5 @@ import { EventEmitter, Injectable } from '@angular/core'; -import { DEFAULT_OWNER, ExportHeaderType, IColumnInfo, IExportRecord, IgxBaseExporter } from '../exporter-common/base-export-service'; +import { DEFAULT_OWNER, ExportHeaderType, ExportRecordType, IColumnInfo, IExportRecord, IgxBaseExporter } from '../exporter-common/base-export-service'; import { ExportUtilities } from '../exporter-common/export-utilities'; import { CharSeparatedValueData } from './char-separated-value-data'; import { CsvFileTypes, IgxCsvExporterOptions } from './csv-exporter-options'; @@ -50,14 +50,40 @@ export class IgxCsvExporterService extends IgxBaseExporter { private _stringData: string; protected exportDataImplementation(data: IExportRecord[], options: IgxCsvExporterOptions, done: () => void) { - const dimensionKeys = data[0]?.dimensionKeys; - data = dimensionKeys?.length ? - data.map((item) => item.rawData): - data.map((item) => item.data); + const firstDataElement = data[0]; + const dimensionKeys = firstDataElement?.dimensionKeys; + + const dataRecords = dimensionKeys?.length ? + data.filter(item => item.type !== ExportRecordType.SummaryRecord).map((item) => item.rawData): + data.filter(item => item.type !== ExportRecordType.SummaryRecord).map((item) => item.data); + + // Get summary records if exportSummaries is enabled + const summaryRecords: any[] = []; + if (options.exportSummaries) { + const summaries = data.filter(item => item.type === ExportRecordType.SummaryRecord); + for (const summary of summaries) { + // Convert summary record data to a flat object format for CSV + const summaryData: any = {}; + if (summary.data) { + for (const [key, value] of Object.entries(summary.data)) { + if (value && typeof value === 'object' && 'label' in value && 'value' in value) { + summaryData[key] = `${value.label}: ${value.value}`; + } else { + summaryData[key] = value; + } + } + } + summaryRecords.push(summaryData); + } + } + + // Combine data records and summary records + const allRecords = [...dataRecords, ...summaryRecords]; + const columnList = this._ownersMap.get(DEFAULT_OWNER); const columns = columnList?.columns.filter(c => c.headerType === ExportHeaderType.ColumnHeader); if (dimensionKeys) { - const dimensionCols = dimensionKeys.map((key) => { + const dimensionCols = dimensionKeys.map((key) => { const columnInfo: IColumnInfo = { header: key, field: key, @@ -72,13 +98,13 @@ export class IgxCsvExporterService extends IgxBaseExporter { columns.unshift(...dimensionCols); } - const csvData = new CharSeparatedValueData(data, options.valueDelimiter, columns); + const csvData = new CharSeparatedValueData(allRecords, options.valueDelimiter, columns); csvData.prepareDataAsync((r) => { this._stringData = r; this.saveFile(options); this.exportEnded.emit({ csvData: this._stringData }); done(); - }); + }, options.alwaysExportHeaders); } private saveFile(options: IgxCsvExporterOptions) {