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..aea2416c319 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 @@ -52,7 +52,10 @@ export class CharSeparatedValueData { this._escapeCharacters.push(this._delimiter); const headers = columns && columns.length ? - columns.map(c => c.header ?? c.field) : + /* When column groups are present, always use the field as it indicates the group the column belongs to. + * Otherwise, in PivotGrid scenarios we can end up with many duplicated column names without a hint what they represent. + */ + columns.map(c => c.columnGroupParent ? c.field : c.header ?? c.field) : keys; this._headerRecord = this.processHeaderRecord(headers, this._data.length); 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 948d811adba..91461d6cfd1 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 @@ -22,6 +22,9 @@ import { FilteringLogic } from '../../data-operations/filtering-expression.inter import { configureTestSuite } from '../../test-utils/configure-suite'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { wait } from '../../test-utils/ui-interactions.spec'; +import { IgxPivotGridComponent } from '../../grids/pivot-grid/pivot-grid.component'; +import { IgxPivotGridTestBaseComponent } from '../../test-utils/pivot-grid-samples.spec'; +import { IgxPivotNumericAggregate } from '../../grids/pivot-grid/pivot-grid-aggregate'; describe('CSV Grid Exporter', () => { configureTestSuite(); @@ -513,6 +516,48 @@ describe('CSV Grid Exporter', () => { }); }); + describe('Pivot Grid CSV export', () => { + let fix; + let pivotGrid: IgxPivotGridComponent; + beforeEach(() => { + fix = TestBed.createComponent(IgxPivotGridTestBaseComponent); + fix.detectChanges(); + pivotGrid = fix.componentInstance.pivotGrid; + pivotGrid.pivotConfiguration = { + columns: [ + { + enabled: true, + memberName: 'Country' + } + ], + rows: [ + { + enabled: true, + memberName: 'ProductCategory' + } + ], + values: [ + { + enabled: true, + member: 'UnitsSold', + aggregate: { + aggregator: IgxPivotNumericAggregate.sum, + key: 'SUM', + label: 'Sum', + }, + } + ] + }; + fix.detectChanges(); + }); + + it('should export pivot grid successfully.', async () => { + await wait(); + const wrapper = await getExportedData(pivotGrid, options); + wrapper.verifyData(wrapper.pivotGridData); + }); + }); + const getExportedData = (grid, csvOptions: IgxCsvExporterOptions) => { const result = new Promise((resolve) => { exporter.exportEnded.pipe(first()).subscribe((value) => { 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 df27d8f41e4..d6a5c9596ae 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, IExportRecord, IgxBaseExporter } from '../exporter-common/base-export-service'; +import { DEFAULT_OWNER, ExportHeaderType, 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,10 +50,29 @@ export class IgxCsvExporterService extends IgxBaseExporter { private _stringData: string; protected exportDataImplementation(data: IExportRecord[], options: IgxCsvExporterOptions, done: () => void) { - data = data.map((item) => item.data); + const dimensionKeys = data[0]?.dimensionKeys; + data = dimensionKeys?.length ? + data.map((item) => item.rawData): + data.map((item) => item.data); 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 columnInfo: IColumnInfo = { + header: key, + field: key, + dataType: 'string', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + columnSpan: 1, + startIndex: 0 + }; + return columnInfo; + }); + columns.unshift(...dimensionCols); + } - const csvData = new CharSeparatedValueData(data, options.valueDelimiter, columnList?.columns); + const csvData = new CharSeparatedValueData(data, options.valueDelimiter, columns); csvData.prepareDataAsync((r) => { this._stringData = r; this.saveFile(options); diff --git a/projects/igniteui-angular/src/lib/services/csv/csv-verification-wrapper.spec.ts b/projects/igniteui-angular/src/lib/services/csv/csv-verification-wrapper.spec.ts index c364189061d..105cd01b132 100644 --- a/projects/igniteui-angular/src/lib/services/csv/csv-verification-wrapper.spec.ts +++ b/projects/igniteui-angular/src/lib/services/csv/csv-verification-wrapper.spec.ts @@ -288,4 +288,12 @@ export class CSVWrapper { `B's Beverages${this._delimiter}Victoria Ashworth${this._delimiter}Fauntleroy Circus${this._delimiter}0${this._delimiter}` + `2500${this._delimiter}5000${this._eor}`; } + + public get pivotGridData() { + return `ProductCategory${this._delimiter}Bulgaria${this._delimiter}USA${this._delimiter}Uruguay${this._eor}` + + `Accessories${this._delimiter}${this._delimiter}293${this._delimiter}${this._eor}` + + `Bikes${this._delimiter}${this._delimiter}${this._delimiter}68${this._eor}` + + `Clothing${this._delimiter}774${this._delimiter}296${this._delimiter}456${this._eor}` + + `Components${this._delimiter}${this._delimiter}240${this._delimiter}${this._eor}`; + } } diff --git a/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts b/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts index 31fe7f6f1ef..189b4290623 100644 --- a/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts +++ b/projects/igniteui-angular/src/lib/services/exporter-common/base-export-service.ts @@ -44,6 +44,9 @@ export interface IExportRecord { summaryKey?: string; hierarchicalOwner?: string; references?: IColumnInfo[]; + /* Adding `rawData` and `dimesnionKeys` properties to support properly exporting pivot grid data to CSV. */ + rawData?: any; + dimensionKeys?: string[]; } export interface IColumnList { @@ -448,6 +451,7 @@ export abstract class IgxBaseExporter { if (!isSpecialData) { const owner = record.owner === undefined ? DEFAULT_OWNER : record.owner; const ownerCols = this._ownersMap.get(owner).columns; + const hasRowHeaders = ownerCols.some(c => c.headerType === ExportHeaderType.RowHeader); if (record.type !== ExportRecordType.HeaderRecord) { const columns = ownerCols @@ -455,6 +459,10 @@ export abstract class IgxBaseExporter { .sort((a, b) => a.startIndex - b.startIndex) .sort((a, b) => a.pinnedIndex - b.pinnedIndex); + if (hasRowHeaders) { + record.rawData = record.data; + } + record.data = columns.reduce((a, e) => { if (!e.skip) { let rawValue = resolveNestedPath(record.data, e.field); @@ -592,6 +600,10 @@ export abstract class IgxBaseExporter { this.flatRecords.push(pivotGridRecord); } + + if (this.flatRecords.length) { + this.flatRecords[0].dimensionKeys = Object.values(this.pivotGridRowDimensionsMap); + } } private prepareHierarchicalGridData(grid: GridType, hasFiltering: boolean, hasSorting: boolean) { @@ -1342,8 +1354,8 @@ export abstract class IgxBaseExporter { for (const k of Object.keys(groupedRecords)) { groupedRecords[k] = groupedRecords[k].filter(row => mapKeys.every(mk => Object.keys(row).includes(mk)) - && mapValues.every(mv => Object.values(row).includes(mv))); - + && mapValues.every(mv => Object.values(row).includes(mv))); + if (groupedRecords[k].length === 0) { delete groupedRecords[k]; }