From ac06346bceae437af34da7835a103942e5528314 Mon Sep 17 00:00:00 2001 From: IBarakov Date: Thu, 25 Mar 2021 13:12:50 +0200 Subject: [PATCH 01/15] feat(igx-hierarchical-grid): export hierarchical grid --- .../src/lib/services/excel/excel-exporter.ts | 2 +- .../src/lib/services/excel/excel-files.ts | 179 +++++++++- .../src/lib/services/excel/excel-strings.ts | 31 +- .../src/lib/services/excel/worksheet-data.ts | 25 +- .../exporter-common/base-export-service.ts | 328 +++++++++++++----- 5 files changed, 461 insertions(+), 104 deletions(-) diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts index dc003310b45..d0f8672c4df 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts @@ -86,7 +86,7 @@ export class IgxExcelExporterService extends IgxBaseExporter { } } - const worksheetData = new WorksheetData(data, this.columnWidthList, options, this._indexOfLastPinnedColumn, this._sort); + const worksheetData = new WorksheetData(data, options, this._sort, this._ownersMap); this._xlsx = new (JSZip as any).default(); diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts index 8147c9902db..894f36e6cf2 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts @@ -4,6 +4,7 @@ import { WorksheetData } from './worksheet-data'; import * as JSZip from 'jszip'; import { yieldingLoop } from '../../core/utils'; +import { ExportRecordType } from '../exporter-common/base-export-service'; /** * @hidden @@ -60,19 +61,170 @@ export class WorksheetFile implements IExcelFile { private dimension = ''; private freezePane = ''; private rowHeight = ''; + private globalCounter = 0; public writeElement() {} public async writeElementAsync(folder: JSZip, worksheetData: WorksheetData) { return new Promise(resolve => { - this.prepareDataAsync(worksheetData, (cols, rows) => { - const hasTable = !worksheetData.isEmpty && worksheetData.options.exportAsTable; + debugger + if (worksheetData.data[0].type === ExportRecordType.HierarchicalGridRecord) { + this.prepareHierarchicalDataAsync(worksheetData, (cols, rows) => { + const hasTable = !worksheetData.isEmpty && worksheetData.options.exportAsTable; + + folder.file('sheet1.xml', ExcelStrings.getSheetXML( + this.dimension, this.freezePane, cols, rows, hasTable, this.maxOutlineLevel)); + resolve(); + }); + } else { + this.prepareDataAsync(worksheetData, (cols, rows) => { + const hasTable = !worksheetData.isEmpty && worksheetData.options.exportAsTable; + + folder.file('sheet1.xml', ExcelStrings.getSheetXML( + this.dimension, this.freezePane, cols, rows, hasTable, this.maxOutlineLevel)); + resolve(); + }); + } + }); + } + + private prepareHierarchicalDataAsync(worksheetData: WorksheetData, done: (cols: string, sheetData: string) => void) { + let sheetData = ''; + let cols = ''; + const dictionary = worksheetData.dataDictionary; + + if (worksheetData.isEmpty) { + sheetData += ''; + this.dimension = 'A1'; + done('', sheetData); + } else { + sheetData += ''; + const height = worksheetData.options.rowHeight; + this.rowHeight = height ? ' ht="' + height + '" customHeight="1"' : ''; + + sheetData += ``; + + for (let i = 0; i < worksheetData.columnCount; i++) { + const column = ExcelStrings.getExcelColumn(i) + 1; + const value = dictionary.saveValue(worksheetData.keys[i], i, true); + sheetData += `${value}`; + } + sheetData += ''; + cols += ''; + + // for (let i = 0; i < worksheetData.columnCount; i++) { + // const width = dictionary.columnWidths[i]; + // // Use the width provided in the options if it exists + // let widthInTwips = worksheetData.options.columnWidth !== undefined ? + // worksheetData.options.columnWidth : + // Math.max(((width / 96) * 14.4), WorksheetFile.MIN_WIDTH); + // if (!(widthInTwips > 0)) { + // widthInTwips = WorksheetFile.MIN_WIDTH; + // } + + // cols += ``; + // } + + const maxColumnSize = worksheetData.data + .filter(rec => rec.type === ExportRecordType.HeaderRecord) + .map(a => a.data.length + a.level) + .sort((a,b) => b - a)[0]; + + cols += ``; + + cols += ''; + + //const indexOfLastPinnedColumn = worksheetData.owners.values().next().value.indexOfLastPinnedColumn; + // const indexOfLastPinnedColumn = -1; + + // if (indexOfLastPinnedColumn !== -1 && + // !worksheetData.options.ignorePinning && + // !worksheetData.options.ignoreColumnsOrder) { + // const frozenColumnCount = indexOfLastPinnedColumn + 1; + // const firstCell = ExcelStrings.getExcelColumn(frozenColumnCount) + '1'; + // this.freezePane = + // ``; + // } - folder.file('sheet1.xml', ExcelStrings.getSheetXML( - this.dimension, this.freezePane, cols, rows, hasTable, this.maxOutlineLevel)); - resolve(); + this.processDataRecordsAsync(worksheetData, (rows) => { + sheetData += rows; + sheetData += ''; + done(cols, sheetData); }); - }); + } + } + + private processHierarchicalRow(worksheetData: WorksheetData, i: number) { + const rowData = new Array(worksheetData.columnCount + 2); + const record = worksheetData.data[i - 1]; + const sHidden = record.hidden ? ` hidden="1"` : ''; + const rowLevel = record.level; + const outlineLevel = rowLevel > 0 ? ` outlineLevel="${rowLevel}"` : ''; + + this.maxOutlineLevel = this.maxOutlineLevel < rowLevel ? rowLevel : this.maxOutlineLevel; + + rowData[0] = ``; + + if (record.type === ExportRecordType.HeaderRecord) { + for (let j = 0; j < record.data.length; j++) { + const excelColumn = record.level + j; + const column = ExcelStrings.getExcelColumn(excelColumn) + (i + 1); + const value = worksheetData.dataDictionary.saveValue(record.data[j].header, i, true); + rowData[j + 1] = `${value}`; + } + + rowData[record.data.length + 1] = ''; + } else { + const keys = Object.keys(record.data); + + for (let j = 0; j < keys.length; j++) { + const cellData = WorksheetFile.getHierarchicalCellData(worksheetData, i, j, record.level, keys[j], this.globalCounter); + rowData[j + 1] = cellData; + this.globalCounter++; + } + + rowData[keys.length + 1] = ''; + } + + + return rowData.join(''); + } + + /* eslint-disable @typescript-eslint/member-ordering */ + private static getHierarchicalCellData(worksheetData: WorksheetData, row: number, + column: number, level: number, key: string, globalCounter: number): string { + const dictionary = worksheetData.dataDictionary; + const columnName = ExcelStrings.getExcelColumn(column + level) + (row + 1); + const fullRow = worksheetData.data[row - 1]; + + debugger; + + const cellValue = worksheetData.isSpecialData ? + fullRow.data : + fullRow.data[key]; + + if (cellValue === undefined || cellValue === null) { + return ``; + } else { + const savedValue = dictionary.saveValue(cellValue, globalCounter, false); + const isSavedAsString = savedValue !== -1; + + const isSavedAsDate = !isSavedAsString && cellValue instanceof Date; + + let value = isSavedAsString ? savedValue : cellValue; + + if (isSavedAsDate) { + const timeZoneOffset = value.getTimezoneOffset() * 60000; + const isoString = (new Date(value - timeZoneOffset)).toISOString(); + value = isoString.substring(0, isoString.indexOf('.')); + } + + const type = isSavedAsString ? ` t="s"` : isSavedAsDate ? ` t="d"` : ''; + + const format = isSavedAsString ? '' : isSavedAsDate ? ` s="2"` : ` s="1"`; + + return `${value}`; + } } private prepareDataAsync(worksheetData: WorksheetData, done: (cols: string, sheetData: string) => void) { @@ -115,10 +267,12 @@ export class WorksheetFile implements IExcelFile { cols += ''; - if (worksheetData.indexOfLastPinnedColumn !== -1 && + //const indexOfLastPinnedColumn = worksheetData.owners.values().next().value.indexOfLastPinnedColumn; + const indexOfLastPinnedColumn = -1; + if (indexOfLastPinnedColumn !== -1 && !worksheetData.options.ignorePinning && !worksheetData.options.ignoreColumnsOrder) { - const frozenColumnCount = worksheetData.indexOfLastPinnedColumn + 1; + const frozenColumnCount = indexOfLastPinnedColumn + 1; const firstCell = ExcelStrings.getExcelColumn(frozenColumnCount) + '1'; this.freezePane = ``; } @@ -138,7 +292,11 @@ export class WorksheetFile implements IExcelFile { yieldingLoop(worksheetData.rowCount - 1, 1000, (i) => { - rowDataArr[i] = this.processRow(worksheetData, i + 1); + if (worksheetData.data[0].type === ExportRecordType.HierarchicalGridRecord) { + rowDataArr[i] = this.processHierarchicalRow(worksheetData, i + 1); + } else { + rowDataArr[i] = this.processRow(worksheetData, i + 1); + } }, () => { done(rowDataArr.join('')); @@ -210,7 +368,8 @@ export class StyleFile implements IExcelFile { public writeElement(folder: JSZip, worksheetData: WorksheetData) { const hasNumberValues = worksheetData.dataDictionary && worksheetData.dataDictionary.hasNumberValues; const hasDateValues = worksheetData.dataDictionary && worksheetData.dataDictionary.hasDateValues; - folder.file('styles.xml', ExcelStrings.getStyles(hasNumberValues, hasDateValues)); + const isHierarchicalGrid = worksheetData.data[0].type === ExportRecordType.HierarchicalGridRecord; + folder.file('styles.xml', ExcelStrings.getStyles(hasNumberValues, hasDateValues, isHierarchicalGrid)); } } diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts b/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts index 33345512ea2..d33bdc42522 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts @@ -20,15 +20,36 @@ export class ExcelStrings { return ExcelStrings.XML_STRING + ''; } - public static getStyles(hasNumberValues: boolean, hasDateValues: boolean): string { - const cellXFCount = hasDateValues ? 3 : hasNumberValues ? 2 : 1; + public static getStyles(hasNumberValues: boolean, hasDateValues: boolean, isHierarchicalGrid: boolean): string { + let cellXFCount = hasDateValues ? 3 : hasNumberValues ? 2 : 1; + const fontsCount = isHierarchicalGrid ? 2 : 1; + const fillsCount = isHierarchicalGrid ? 3 : 2; + + const additionalFill = fillsCount === 3 ? + ' ' : + ''; + + const additionalFont = fontsCount === 2 ? + '' : + ''; + + + const fills = `${additionalFill}`; + const fonts = `${additionalFont}`; + let additionalCellXF = ''; if (hasDateValues) { additionalCellXF = additionalCellXF + ' '; } - return ExcelStrings.XML_STRING + '' + additionalCellXF + ''; + if(isHierarchicalGrid) { + cellXFCount++; + additionalCellXF = additionalCellXF + ' '; + } + + + return ExcelStrings.XML_STRING + '' + fonts + fills + '' + additionalCellXF + ''; } public static getWorkbook(worksheetName: string): string { @@ -59,10 +80,12 @@ export class ExcelStrings { // return ExcelStrings.XML_STRING + // '' + freezePane + '' + cols + sheetData + '' + tableParts + ''; + + // TODO FIX ! + //below sheetoutlineprop - return `${ExcelStrings.XML_STRING} ${sheetOutlineProp} - ${freezePane} ${cols} diff --git a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts index cd989f24425..779f93b7ad4 100644 --- a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts +++ b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts @@ -1,4 +1,4 @@ -import { IExportRecord } from '../exporter-common/base-export-service'; +import { ExportRecordType, IExportRecord, IMapRecord } from '../exporter-common/base-export-service'; import { ExportUtilities } from '../exporter-common/export-utilities'; import { IgxExcelExporterOptions } from './excel-exporter-options'; import { WorksheetDataDictionary } from './worksheet-data-dictionary'; @@ -11,8 +11,10 @@ export class WorksheetData { private _keys: string[]; private _isSpecialData: boolean; - constructor(private _data: IExportRecord[], private _columnWidths: number[], public options: IgxExcelExporterOptions, - public indexOfLastPinnedColumn: number, public sort: any) { + constructor(private _data: IExportRecord[], + public options: IgxExcelExporterOptions, + public sort: any, + public owners: Map) { this.initializeData(); } @@ -48,10 +50,21 @@ export class WorksheetData { if (!this._data || this._data.length === 0) { return; } + debugger + const columnWidths = this.owners.values().next().value.columnWidths; + const actualData = this._data.filter(item => item.type !== ExportRecordType.HeaderRecord).map(item => item.data); - const actualData = this._data.map(item => item.data); + if (this._data[0].type === ExportRecordType.HierarchicalGridRecord) { + this.options.exportAsTable = false; + + const hierarchicalGridData = + this._data.filter(item => item.type === ExportRecordType.HierarchicalGridRecord).map(item => item.data); + + this._keys = ExportUtilities.getKeysFromData(hierarchicalGridData); // TODO refactor + } else { + this._keys = ExportUtilities.getKeysFromData(actualData); + } - this._keys = ExportUtilities.getKeysFromData(actualData); if (this._keys.length === 0) { return; } @@ -61,6 +74,6 @@ export class WorksheetData { this._columnCount = this._keys.length; this._rowCount = this._data.length + 1; - this._dataDictionary = new WorksheetDataDictionary(this._columnCount, this.options.columnWidth, this._columnWidths); + this._dataDictionary = new WorksheetDataDictionary(this._columnCount, this.options.columnWidth, columnWidths); } } 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 8506b40ba8f..c415d3b354c 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 @@ -1,8 +1,6 @@ import { EventEmitter } from '@angular/core'; - import { cloneArray, cloneValue, IBaseEventArgs, resolveNestedPath, yieldingLoop } from '../../core/utils'; import { DataUtil } from '../../data-operations/data-util'; - import { ExportUtilities } from './export-utilities'; import { IgxExporterOptionsBase } from './exporter-options-base'; import { ITreeGridRecord } from '../../grids/tree-grid/tree-grid.interfaces'; @@ -16,11 +14,15 @@ import { IgxTreeGridComponent } from '../../grids/tree-grid/public_api'; import { IgxGridComponent } from '../../grids/grid/public_api'; import { DatePipe } from '@angular/common'; import { IGroupByRecord } from '../../data-operations/groupby-record.interface'; +import { IgxHierarchicalGridComponent } from '../../grids/hierarchical-grid/hierarchical-grid.component'; +import { IgxRowIslandComponent } from './../../../../../../dist/igniteui-angular/esm2015/lib/grids/hierarchical-grid/row-island.component'; export enum ExportRecordType { GroupedRecord = 1, TreeGridRecord = 2, DataRecord = 3, + HierarchicalGridRecord = 4, + HeaderRecord = 5, } export interface IExportRecord { @@ -28,6 +30,13 @@ export interface IExportRecord { level: number; hidden?: boolean; type: ExportRecordType; + owner?: any; +} + +export interface IMapRecord { + indexOfLastPinnedColumn: number; + columnWidths: number[]; + columns: any; } /** @@ -91,6 +100,7 @@ export interface IColumnExportingEventArgs extends IBaseEventArgs { const DEFAULT_COLUMN_WIDTH = 8.43; export abstract class IgxBaseExporter { + public exportEnded = new EventEmitter(); /** @@ -117,16 +127,11 @@ export abstract class IgxBaseExporter { */ public columnExporting = new EventEmitter(); - protected _indexOfLastPinnedColumn = -1; protected _sort = null; + protected _ownersMap: Map = new Map(); - private _columnList: any[]; - private _columnWidthList: number[]; private flatRecords: IExportRecord[] = []; - - public get columnWidthList() { - return this._columnWidthList; - } + private options: IgxExporterOptionsBase; /** * Method for exporting IgxGrid component's data. @@ -140,46 +145,31 @@ export abstract class IgxBaseExporter { if (options === undefined || options === null) { throw Error('No options provided!'); } - + debugger + this.options = options; const columns = grid.columnList.toArray(); - this._columnList = new Array(columns.length); - this._columnWidthList = new Array(columns.filter(c => !c.hidden).length); + const colDefinitions = this.getColumns(columns, options); - const hiddenColumns = []; - let lastVisibleColumnIndex = -1; + const mapRecord: IMapRecord = { + columns: colDefinitions.colList, + columnWidths: colDefinitions.colWidthList, + indexOfLastPinnedColumn: colDefinitions.indexOfLastPinnedColumn, + }; - columns.forEach((column) => { - const columnHeader = !ExportUtilities.isNullOrWhitespaces(column.header) ? column.header : column.field; - const exportColumn = !column.hidden || options.ignoreColumnsVisibility; - const index = options.ignoreColumnsOrder || options.ignoreColumnsVisibility ? column.index : column.visibleIndex; - const columnWidth = Number(column.width.slice(0, -2)); + const tagName = grid.nativeElement.tagName.toLowerCase(); - const columnInfo = { - header: columnHeader, - dataType: column.dataType, - field: column.field, - skip: !exportColumn, - formatter: column.formatter, - skipFormatter: false - }; + if (tagName === 'igx-hierarchical-grid') { + this._ownersMap.set(grid, mapRecord); - if (index !== -1) { - this._columnList[index] = columnInfo; - this._columnWidthList[index] = columnWidth; - lastVisibleColumnIndex = Math.max(lastVisibleColumnIndex, index); - } else { - hiddenColumns.push(columnInfo); - } + const keys = (grid as IgxHierarchicalGridComponent).childLayoutKeys; - if (column.pinned && exportColumn) { - this._indexOfLastPinnedColumn++; + for (const key of keys) { + const rowIsland = grid.childLayoutList.filter(l => l.key === key)[0]; + this.mapHierarchicalGridColumns(rowIsland); } - }); - - // Append the hidden columns to the end of the list - hiddenColumns.forEach((hiddenColumn) => { - this._columnList[++lastVisibleColumnIndex] = hiddenColumn; - }); + } else { + this._ownersMap.set('default', mapRecord); + } this.prepareData(grid, options); this.exportGridRecordsData(this.flatRecords, options); @@ -216,47 +206,60 @@ export abstract class IgxBaseExporter { throw Error('No options provided!'); } - if (!this._columnList || this._columnList.length === 0) { + if (this._ownersMap.size === 0) { const recordsData = records.map(r => r.data); const keys = ExportUtilities.getKeysFromData(recordsData); - this._columnList = keys.map((k) => ({ header: k, field: k, skip: false })); - this._columnWidthList = new Array(keys.length).fill(DEFAULT_COLUMN_WIDTH); + const columns = keys.map((k) => ({ header: k, field: k, skip: false })); + const columnWidths = new Array(keys.length).fill(DEFAULT_COLUMN_WIDTH); + + const mapRecord: IMapRecord = { + columns, + columnWidths, + indexOfLastPinnedColumn: -1 + }; + + this._ownersMap.set('default', mapRecord); } - let skippedPinnedColumnsCount = 0; - let columnsWithoutHeaderCount = 1; - this._columnList.forEach((column, index) => { - if (!column.skip) { - const columnExportArgs = { - header: !ExportUtilities.isNullOrWhitespaces(column.header) ? - column.header : - 'Column' + columnsWithoutHeaderCount++, - field: column.field, - columnIndex: index, - cancel: false, - skipFormatter: false - }; - this.columnExporting.emit(columnExportArgs); + for (const [key, mapRecord] of this._ownersMap) { + let skippedPinnedColumnsCount = 0; + let columnsWithoutHeaderCount = 1; + let indexOfLastPinnedColumn = mapRecord.indexOfLastPinnedColumn; + + mapRecord.columns.forEach((column, index) => { + if (!column.skip) { + const columnExportArgs = { + header: !ExportUtilities.isNullOrWhitespaces(column.header) ? + column.header : + 'Column' + columnsWithoutHeaderCount++, + field: column.field, + columnIndex: index, + cancel: false, + skipFormatter: false, + owner: key === 'default' ? undefined : key + }; + this.columnExporting.emit(columnExportArgs); - column.header = columnExportArgs.header; - column.skip = columnExportArgs.cancel; - column.skipFormatter = columnExportArgs.skipFormatter; + column.header = columnExportArgs.header; + column.skip = columnExportArgs.cancel; + column.skipFormatter = columnExportArgs.skipFormatter; - if (column.skip && index <= this._indexOfLastPinnedColumn) { - skippedPinnedColumnsCount++; - } + if (column.skip && index <= indexOfLastPinnedColumn) { + skippedPinnedColumnsCount++; + } - if (this._sort && this._sort.fieldName === column.field) { - if (column.skip) { - this._sort = null; - } else { - this._sort.fieldName = column.header; + if (this._sort && this._sort.fieldName === column.field) { + if (column.skip) { + this._sort = null; + } else { + this._sort.fieldName = column.header; + } } } - } - }); + }); - this._indexOfLastPinnedColumn -= skippedPinnedColumnsCount; + indexOfLastPinnedColumn -= skippedPinnedColumnsCount; + } const dataToExport = new Array(); const actualData = records.map(r => r.data); @@ -272,8 +275,12 @@ export abstract class IgxBaseExporter { } private exportRow(data: IExportRecord[], record: IExportRecord, index: number, isSpecialData: boolean) { - if (!isSpecialData) { - record.data = this._columnList.reduce((a, e) => { + if (!isSpecialData && record.type !== ExportRecordType.HeaderRecord) { + const columns = record.owner === undefined ? + this._ownersMap.get('default').columns : + this._ownersMap.get(record.owner).columns; + + record.data = columns.reduce((a, e) => { if (!e.skip) { let rawValue = resolveNestedPath(record.data, e.field); @@ -318,11 +325,97 @@ export abstract class IgxBaseExporter { const hasSorting = grid.sortingExpressions && grid.sortingExpressions.length > 0; + if (tagName === 'igx-hierarchical-grid') { + this.prepareHierarchicalGridData(grid as IgxHierarchicalGridComponent); + } else { + if (tagName === 'igx-grid') { + this.prepareGridData(grid as IgxGridComponent, options, hasFiltering, hasSorting); + } - if (tagName === 'igx-grid') { - this.prepareGridData(grid as IgxGridComponent, options, hasFiltering, hasSorting); - } if (tagName === 'igx-tree-grid') { - this.prepareTreeGridData(grid as IgxTreeGridComponent, options, hasFiltering, hasSorting); + if (tagName === 'igx-tree-grid') { + this.prepareTreeGridData(grid as IgxTreeGridComponent, options, hasFiltering, hasSorting); + } + } + } + + private prepareHierarchicalGridData(grid: IgxHierarchicalGridComponent, level: number = 0) { + //options: IgxExporterOptionsBase, hasFiltering: boolean, hasSorting: boolean + const records = grid.data; + this.addHierarchicalGridData(grid, records, level); + } + + private addHierarchicalGridData(grid: IgxHierarchicalGridComponent, records: any[], level: number) { + const childLayoutKeys = grid.childLayoutKeys; + const childGrids = grid.hgridAPI.getChildGrids(true); + + for(const entry of records) { + const expansionStateVal = grid.expansionStates.has(entry) ? grid.expansionStates.get(entry) : false; + + const dataWithoutChildren = Object.keys(entry) + .filter(k => !childLayoutKeys.includes(k)) + .reduce((obj, key) => { + obj[key] = entry[key]; + return obj; + }, {}); + + const firstRow: IExportRecord = { + data: dataWithoutChildren, + level, + type: ExportRecordType.HierarchicalGridRecord, + owner: grid, + }; + + this.flatRecords.push(firstRow); + + for (const key of childLayoutKeys) { + const island = grid.childLayoutList.filter(l => l.key === key)[0]; + const keyRecordData = entry[key] || []; + + this.getAllChildColumnsAndData(island, keyRecordData, expansionStateVal, childGrids); + } + } + } + + private getAllChildColumnsAndData(island: IgxRowIslandComponent, childData: any, expansionStateVal: boolean, childGrids: any) { + const islandColumnList = island.childColumns.toArray(); + const modifiedColumns = this.getColumns(islandColumnList, this.options); + + const headerRecord: IExportRecord = { + data: modifiedColumns.colList, + level: island.level, + type: ExportRecordType.HeaderRecord, + owner: island, + hidden: !expansionStateVal + }; + + if (childData && childData.length > 0) { + this.flatRecords.push(headerRecord); + + for (const rec of childData) { + const exportRecord: IExportRecord = { + data: rec, + level: island.level, + type: ExportRecordType.DataRecord, + owner: island, + hidden: !expansionStateVal + }; + + this.flatRecords.push(exportRecord); + + if (island.children.length > 0) { + for (const childIsland of island.children) { + const islandGrid = childGrids.filter(g => g.key === island.key); + + const islandExpansionStateVal = islandGrid.length === 0 ? + false : + islandGrid[0].expansionStates.has(rec) ? + islandGrid[0].expansionStates.get(rec) : // TODO get grid by row + false; + + this.getAllChildColumnsAndData(childIsland, rec[childIsland.key], islandExpansionStateVal, childGrids); + } + } + } } } @@ -454,7 +547,7 @@ export abstract class IgxBaseExporter { return; } - const firstCol = this._columnList[0].field; + const firstCol = this._ownersMap.get(grid).columns[0].field; for (const record of records) { let recordVal = record.value; @@ -484,6 +577,7 @@ export abstract class IgxBaseExporter { level: record.level, hidden: !parentExpanded, type: ExportRecordType.GroupedRecord, + owner: grid }; this.flatRecords.push(groupExpression); @@ -498,7 +592,8 @@ export abstract class IgxBaseExporter { data: rowRecord, level: record.level + 1, hidden: !(expanded && parentExpanded), - type: ExportRecordType.DataRecord + type: ExportRecordType.DataRecord, + owner: grid }; this.flatRecords.push(currentRecord); @@ -507,11 +602,78 @@ export abstract class IgxBaseExporter { } } + private getColumns(columns: any, options: IgxExporterOptionsBase): any { + const colList = new Array(columns.length); + const colWidthList = new Array(columns.filter(c => !c.hidden).length); + const hiddenColumns = []; + let indexOfLastPinnedColumn = -1; + let lastVisibleColumnIndex = -1; + + columns.forEach((column, i) => { + const columnHeader = !ExportUtilities.isNullOrWhitespaces(column.header) ? column.header : column.field; + const exportColumn = !column.hidden || options.ignoreColumnsVisibility; + const index = options.ignoreColumnsOrder || options.ignoreColumnsVisibility ? column.index : column.visibleIndex; + const columnWidth = Number(column.width?.slice(0, -2)) || DEFAULT_COLUMN_WIDTH; + + const columnInfo = { + header: columnHeader, + dataType: column.dataType, + field: column.field, + skip: !exportColumn, + formatter: column.formatter, + skipFormatter: false + }; + + if (index !== -1) { + colList[index] = columnInfo; + colWidthList[index] = columnWidth; + lastVisibleColumnIndex = Math.max(lastVisibleColumnIndex, index); + } else { + hiddenColumns.push(columnInfo); + } + + if (column.pinned && exportColumn) { + indexOfLastPinnedColumn++; + } + }); + + // Append the hidden columns to the end of the list + hiddenColumns.forEach((hiddenColumn) => { + colList[++lastVisibleColumnIndex] = hiddenColumn; + }); + + const result = { + colList, + colWidthList, + indexOfLastPinnedColumn + }; + + return result; + } + + private mapHierarchicalGridColumns(island: IgxRowIslandComponent) { + const islandColumnList = island.childColumns.toArray(); + const colDefinitions = this.getColumns(islandColumnList, this.options); + + const mapRecord: IMapRecord = { + columns: colDefinitions.colList, + columnWidths: colDefinitions.colWidthList, + indexOfLastPinnedColumn: colDefinitions.indexOfLastPinnedColumn, + }; + + this._ownersMap.set(island, mapRecord); + + if (island.children.length > 0) { + for (const childIsland of island.children) { + this.mapHierarchicalGridColumns(childIsland); + } + } + } + private resetDefaults() { - this._columnList = []; - this._indexOfLastPinnedColumn = -1; this._sort = null; this.flatRecords = []; + this._ownersMap.clear(); } protected abstract exportDataImplementation(data: any[], options: IgxExporterOptionsBase): void; From 42b0ec53685aff379d0193f466f2e48959a2c8ad Mon Sep 17 00:00:00 2001 From: IBarakov Date: Tue, 30 Mar 2021 13:11:02 +0300 Subject: [PATCH 02/15] feat(hierarchical-grid): excel export major refactoring --- .../src/lib/services/excel/excel-exporter.ts | 30 +- .../src/lib/services/excel/excel-files.ts | 266 +++++------------- .../src/lib/services/excel/worksheet-data.ts | 33 +-- .../exporter-common/base-export-service.ts | 13 +- 4 files changed, 109 insertions(+), 233 deletions(-) diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts index d0f8672c4df..e89d9bb5099 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts @@ -5,7 +5,7 @@ import { ExcelElementsFactory } from './excel-elements-factory'; import { ExcelFolderTypes } from './excel-enums'; import { IgxExcelExporterOptions } from './excel-exporter-options'; import { IExcelFolder } from './excel-interfaces'; -import { IExportRecord, IgxBaseExporter } from '../exporter-common/base-export-service'; +import { ExportRecordType, IExportRecord, IgxBaseExporter } from '../exporter-common/base-export-service'; import { ExportUtilities } from '../exporter-common/export-utilities'; import { WorksheetData } from './worksheet-data'; import { IBaseEventArgs } from '../../core/utils'; @@ -72,9 +72,14 @@ export class IgxExcelExporterService extends IgxBaseExporter { } protected exportDataImplementation(data: IExportRecord[], options: IgxExcelExporterOptions): void { - const level = data[0]?.level; - - if (typeof level !== 'undefined') { + const firstDataElement = data[0]; + let rootKeys; + let columnCount; + let columnWidths; + let maxColumnSize; + let indexOfLastPinnedColumn; + + if (typeof firstDataElement !== 'undefined') { let maxLevel = 0; data.forEach((r) => { @@ -84,9 +89,24 @@ export class IgxExcelExporterService extends IgxBaseExporter { if (maxLevel > 7) { throw Error('Can create an outline of up to eight levels!'); } + + if (firstDataElement.type === ExportRecordType.HierarchicalGridRecord) { + maxColumnSize = data + .filter(rec => rec.type === ExportRecordType.HeaderRecord) + .map(a => a.data.length + a.level) + .sort((a,b) => b - a)[0]; + } else { + const defaultOwner = this._ownersMap.get('default'); + columnWidths = defaultOwner.columnWidths; + indexOfLastPinnedColumn = defaultOwner.indexOfLastPinnedColumn; + } + + rootKeys = Object.keys(firstDataElement.data); + columnCount = Object.keys(firstDataElement.data).length; } - const worksheetData = new WorksheetData(data, options, this._sort, this._ownersMap); + const worksheetData = + new WorksheetData(data, options, this._sort, columnCount, rootKeys, indexOfLastPinnedColumn, columnWidths, maxColumnSize); this._xlsx = new (JSZip as any).default(); diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts index 894f36e6cf2..0dbbacbbe3f 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts @@ -62,169 +62,20 @@ export class WorksheetFile implements IExcelFile { private freezePane = ''; private rowHeight = ''; private globalCounter = 0; + private isHierarchicalGrid; public writeElement() {} public async writeElementAsync(folder: JSZip, worksheetData: WorksheetData) { return new Promise(resolve => { - debugger - if (worksheetData.data[0].type === ExportRecordType.HierarchicalGridRecord) { - this.prepareHierarchicalDataAsync(worksheetData, (cols, rows) => { - const hasTable = !worksheetData.isEmpty && worksheetData.options.exportAsTable; - - folder.file('sheet1.xml', ExcelStrings.getSheetXML( - this.dimension, this.freezePane, cols, rows, hasTable, this.maxOutlineLevel)); - resolve(); - }); - } else { - this.prepareDataAsync(worksheetData, (cols, rows) => { - const hasTable = !worksheetData.isEmpty && worksheetData.options.exportAsTable; - - folder.file('sheet1.xml', ExcelStrings.getSheetXML( - this.dimension, this.freezePane, cols, rows, hasTable, this.maxOutlineLevel)); - resolve(); - }); - } - }); - } - - private prepareHierarchicalDataAsync(worksheetData: WorksheetData, done: (cols: string, sheetData: string) => void) { - let sheetData = ''; - let cols = ''; - const dictionary = worksheetData.dataDictionary; - - if (worksheetData.isEmpty) { - sheetData += ''; - this.dimension = 'A1'; - done('', sheetData); - } else { - sheetData += ''; - const height = worksheetData.options.rowHeight; - this.rowHeight = height ? ' ht="' + height + '" customHeight="1"' : ''; + this.prepareDataAsync(worksheetData, (cols, rows) => { + const hasTable = !worksheetData.isEmpty && worksheetData.options.exportAsTable; - sheetData += ``; - - for (let i = 0; i < worksheetData.columnCount; i++) { - const column = ExcelStrings.getExcelColumn(i) + 1; - const value = dictionary.saveValue(worksheetData.keys[i], i, true); - sheetData += `${value}`; - } - sheetData += ''; - cols += ''; - - // for (let i = 0; i < worksheetData.columnCount; i++) { - // const width = dictionary.columnWidths[i]; - // // Use the width provided in the options if it exists - // let widthInTwips = worksheetData.options.columnWidth !== undefined ? - // worksheetData.options.columnWidth : - // Math.max(((width / 96) * 14.4), WorksheetFile.MIN_WIDTH); - // if (!(widthInTwips > 0)) { - // widthInTwips = WorksheetFile.MIN_WIDTH; - // } - - // cols += ``; - // } - - const maxColumnSize = worksheetData.data - .filter(rec => rec.type === ExportRecordType.HeaderRecord) - .map(a => a.data.length + a.level) - .sort((a,b) => b - a)[0]; - - cols += ``; - - cols += ''; - - //const indexOfLastPinnedColumn = worksheetData.owners.values().next().value.indexOfLastPinnedColumn; - // const indexOfLastPinnedColumn = -1; - - // if (indexOfLastPinnedColumn !== -1 && - // !worksheetData.options.ignorePinning && - // !worksheetData.options.ignoreColumnsOrder) { - // const frozenColumnCount = indexOfLastPinnedColumn + 1; - // const firstCell = ExcelStrings.getExcelColumn(frozenColumnCount) + '1'; - // this.freezePane = - // ``; - // } - - this.processDataRecordsAsync(worksheetData, (rows) => { - sheetData += rows; - sheetData += ''; - done(cols, sheetData); + folder.file('sheet1.xml', ExcelStrings.getSheetXML( + this.dimension, this.freezePane, cols, rows, hasTable, this.maxOutlineLevel)); + resolve(); }); - } - } - - private processHierarchicalRow(worksheetData: WorksheetData, i: number) { - const rowData = new Array(worksheetData.columnCount + 2); - const record = worksheetData.data[i - 1]; - const sHidden = record.hidden ? ` hidden="1"` : ''; - const rowLevel = record.level; - const outlineLevel = rowLevel > 0 ? ` outlineLevel="${rowLevel}"` : ''; - - this.maxOutlineLevel = this.maxOutlineLevel < rowLevel ? rowLevel : this.maxOutlineLevel; - - rowData[0] = ``; - - if (record.type === ExportRecordType.HeaderRecord) { - for (let j = 0; j < record.data.length; j++) { - const excelColumn = record.level + j; - const column = ExcelStrings.getExcelColumn(excelColumn) + (i + 1); - const value = worksheetData.dataDictionary.saveValue(record.data[j].header, i, true); - rowData[j + 1] = `${value}`; - } - - rowData[record.data.length + 1] = ''; - } else { - const keys = Object.keys(record.data); - - for (let j = 0; j < keys.length; j++) { - const cellData = WorksheetFile.getHierarchicalCellData(worksheetData, i, j, record.level, keys[j], this.globalCounter); - rowData[j + 1] = cellData; - this.globalCounter++; - } - - rowData[keys.length + 1] = ''; - } - - - return rowData.join(''); - } - - /* eslint-disable @typescript-eslint/member-ordering */ - private static getHierarchicalCellData(worksheetData: WorksheetData, row: number, - column: number, level: number, key: string, globalCounter: number): string { - const dictionary = worksheetData.dataDictionary; - const columnName = ExcelStrings.getExcelColumn(column + level) + (row + 1); - const fullRow = worksheetData.data[row - 1]; - - debugger; - - const cellValue = worksheetData.isSpecialData ? - fullRow.data : - fullRow.data[key]; - - if (cellValue === undefined || cellValue === null) { - return ``; - } else { - const savedValue = dictionary.saveValue(cellValue, globalCounter, false); - const isSavedAsString = savedValue !== -1; - - const isSavedAsDate = !isSavedAsString && cellValue instanceof Date; - - let value = isSavedAsString ? savedValue : cellValue; - - if (isSavedAsDate) { - const timeZoneOffset = value.getTimezoneOffset() * 60000; - const isoString = (new Date(value - timeZoneOffset)).toISOString(); - value = isoString.substring(0, isoString.indexOf('.')); - } - - const type = isSavedAsString ? ` t="s"` : isSavedAsDate ? ` t="d"` : ''; - - const format = isSavedAsString ? '' : isSavedAsDate ? ` s="2"` : ` s="1"`; - - return `${value}`; - } + }); } private prepareDataAsync(worksheetData: WorksheetData, done: (cols: string, sheetData: string) => void) { @@ -237,44 +88,52 @@ export class WorksheetFile implements IExcelFile { this.dimension = 'A1'; done('', sheetData); } else { - sheetData += ''; + this.isHierarchicalGrid = worksheetData.data[0].type === ExportRecordType.HierarchicalGridRecord; + const height = worksheetData.options.rowHeight; - this.rowHeight = height ? ' ht="' + height + '" customHeight="1"' : ''; - sheetData += ``; + const rowStyle = this.isHierarchicalGrid ? ' s="3"' : ''; + this.rowHeight = height ? ` ht="${height}" customHeight="1"` : ''; + + sheetData += ``; for (let i = 0; i < worksheetData.columnCount; i++) { const column = ExcelStrings.getExcelColumn(i) + 1; const value = dictionary.saveValue(worksheetData.keys[i], i, true); - sheetData += `${value}`; + sheetData += `${value}`; } sheetData += ''; - this.dimension = 'A1:' + ExcelStrings.getExcelColumn(worksheetData.columnCount - 1) + worksheetData.rowCount; - cols += ''; - - for (let i = 0; i < worksheetData.columnCount; i++) { - const width = dictionary.columnWidths[i]; - // Use the width provided in the options if it exists - let widthInTwips = worksheetData.options.columnWidth !== undefined ? - worksheetData.options.columnWidth : - Math.max(((width / 96) * 14.4), WorksheetFile.MIN_WIDTH); - if (!(widthInTwips > 0)) { - widthInTwips = WorksheetFile.MIN_WIDTH; + if (!this.isHierarchicalGrid) { + this.dimension = 'A1:' + ExcelStrings.getExcelColumn(worksheetData.columnCount - 1) + worksheetData.rowCount; + cols += ''; + + for (let i = 0; i < worksheetData.columnCount; i++) { + const width = dictionary.columnWidths[i]; + // Use the width provided in the options if it exists + let widthInTwips = worksheetData.options.columnWidth !== undefined ? + worksheetData.options.columnWidth : + Math.max(((width / 96) * 14.4), WorksheetFile.MIN_WIDTH); + if (!(widthInTwips > 0)) { + widthInTwips = WorksheetFile.MIN_WIDTH; + } + + cols += ``; } - cols += ``; - } + cols += ''; - cols += ''; + const indexOfLastPinnedColumn = worksheetData.indexOfLastPinnedColumn; - //const indexOfLastPinnedColumn = worksheetData.owners.values().next().value.indexOfLastPinnedColumn; - const indexOfLastPinnedColumn = -1; - if (indexOfLastPinnedColumn !== -1 && - !worksheetData.options.ignorePinning && - !worksheetData.options.ignoreColumnsOrder) { - const frozenColumnCount = indexOfLastPinnedColumn + 1; - const firstCell = ExcelStrings.getExcelColumn(frozenColumnCount) + '1'; - this.freezePane = ``; + if (indexOfLastPinnedColumn !== -1 && + !worksheetData.options.ignorePinning && + !worksheetData.options.ignoreColumnsOrder) { + const frozenColumnCount = indexOfLastPinnedColumn + 1; + const firstCell = ExcelStrings.getExcelColumn(frozenColumnCount) + '1'; + this.freezePane = + ``; + } + } else { + cols += ``; } this.processDataRecordsAsync(worksheetData, (rows) => { @@ -292,11 +151,7 @@ export class WorksheetFile implements IExcelFile { yieldingLoop(worksheetData.rowCount - 1, 1000, (i) => { - if (worksheetData.data[0].type === ExportRecordType.HierarchicalGridRecord) { - rowDataArr[i] = this.processHierarchicalRow(worksheetData, i + 1); - } else { - rowDataArr[i] = this.processRow(worksheetData, i + 1); - } + rowDataArr[i] = this.processRow(worksheetData, i + 1); }, () => { done(rowDataArr.join('')); @@ -314,31 +169,47 @@ export class WorksheetFile implements IExcelFile { rowData[0] = ``; - for (let j = 0; j < worksheetData.columnCount; j++) { - const cellData = WorksheetFile.getCellData(worksheetData, i, j); - rowData[j + 1] = cellData; - } + if (record.type === ExportRecordType.HeaderRecord) { + for (let j = 0; j < record.data.length; j++) { + const excelColumn = record.level + j; + const column = ExcelStrings.getExcelColumn(excelColumn) + (i + 1); + const value = worksheetData.dataDictionary.saveValue(record.data[j].header, i, true); + rowData[j + 1] = `${value}`; + } - rowData[worksheetData.columnCount + 1] = ''; + rowData[record.data.length + 1] = ''; + } else { + const keys = Object.keys(record.data); + + for (let j = 0; j < keys.length; j++) { + const col = j + (this.isHierarchicalGrid ? record.level : 0); + + const cellData = WorksheetFile.getCellData(worksheetData, i, col, keys[j], this.globalCounter); + + rowData[j + 1] = cellData; + this.globalCounter++; + } + + rowData[keys.length + 1] = ''; + } return rowData.join(''); } /* eslint-disable @typescript-eslint/member-ordering */ - private static getCellData(worksheetData: WorksheetData, row: number, column: number): string { + private static getCellData(worksheetData: WorksheetData, row: number, column: number, key: string, globalCounter: number): string { const dictionary = worksheetData.dataDictionary; const columnName = ExcelStrings.getExcelColumn(column) + (row + 1); - const columnHeader = worksheetData.keys[column]; const fullRow = worksheetData.data[row - 1]; const cellValue = worksheetData.isSpecialData ? fullRow.data : - fullRow.data[columnHeader]; + fullRow.data[key]; if (cellValue === undefined || cellValue === null) { return ``; } else { - const savedValue = dictionary.saveValue(cellValue, column, false); + const savedValue = dictionary.saveValue(cellValue, globalCounter, false); const isSavedAsString = savedValue !== -1; const isSavedAsDate = !isSavedAsString && cellValue instanceof Date; @@ -368,7 +239,8 @@ export class StyleFile implements IExcelFile { public writeElement(folder: JSZip, worksheetData: WorksheetData) { const hasNumberValues = worksheetData.dataDictionary && worksheetData.dataDictionary.hasNumberValues; const hasDateValues = worksheetData.dataDictionary && worksheetData.dataDictionary.hasDateValues; - const isHierarchicalGrid = worksheetData.data[0].type === ExportRecordType.HierarchicalGridRecord; + const isHierarchicalGrid = worksheetData.data[0]?.type === ExportRecordType.HierarchicalGridRecord; + folder.file('styles.xml', ExcelStrings.getStyles(hasNumberValues, hasDateValues, isHierarchicalGrid)); } } diff --git a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts index 779f93b7ad4..ee3c7cf8b79 100644 --- a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts +++ b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts @@ -5,16 +5,18 @@ import { WorksheetDataDictionary } from './worksheet-data-dictionary'; /** @hidden */ export class WorksheetData { - private _columnCount: number; private _rowCount: number; private _dataDictionary: WorksheetDataDictionary; - private _keys: string[]; private _isSpecialData: boolean; constructor(private _data: IExportRecord[], public options: IgxExcelExporterOptions, public sort: any, - public owners: Map) { + public columnCount: number, + public rootKeys: string[], + public indexOfLastPinnedColumn: number, + public columnWidths: number[], + public maxColumnSize: number) { this.initializeData(); } @@ -22,20 +24,16 @@ export class WorksheetData { return this._data; } - public get columnCount(): number { - return this._columnCount; - } - public get rowCount(): number { return this._rowCount; } public get isEmpty(): boolean { - return !this.rowCount || !this._columnCount; + return !this.rowCount || !this.columnCount; } public get keys(): string[] { - return this._keys; + return this.rootKeys; } public get isSpecialData(): boolean { @@ -51,29 +49,14 @@ export class WorksheetData { return; } debugger - const columnWidths = this.owners.values().next().value.columnWidths; const actualData = this._data.filter(item => item.type !== ExportRecordType.HeaderRecord).map(item => item.data); if (this._data[0].type === ExportRecordType.HierarchicalGridRecord) { this.options.exportAsTable = false; - - const hierarchicalGridData = - this._data.filter(item => item.type === ExportRecordType.HierarchicalGridRecord).map(item => item.data); - - this._keys = ExportUtilities.getKeysFromData(hierarchicalGridData); // TODO refactor - } else { - this._keys = ExportUtilities.getKeysFromData(actualData); - } - - if (this._keys.length === 0) { - return; } this._isSpecialData = ExportUtilities.isSpecialData(actualData); - - this._columnCount = this._keys.length; this._rowCount = this._data.length + 1; - - this._dataDictionary = new WorksheetDataDictionary(this._columnCount, this.options.columnWidth, columnWidths); + this._dataDictionary = new WorksheetDataDictionary(this.columnCount, this.options.columnWidth, this.columnWidths); } } 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 c415d3b354c..b4efdbb99a8 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 @@ -28,9 +28,9 @@ export enum ExportRecordType { export interface IExportRecord { data: any; level: number; - hidden?: boolean; type: ExportRecordType; owner?: any; + hidden?: boolean; } export interface IMapRecord { @@ -97,6 +97,7 @@ export interface IColumnExportingEventArgs extends IBaseEventArgs { skipFormatter: boolean; } +const DEFAULT_OWNER = 'default'; const DEFAULT_COLUMN_WIDTH = 8.43; export abstract class IgxBaseExporter { @@ -145,7 +146,7 @@ export abstract class IgxBaseExporter { if (options === undefined || options === null) { throw Error('No options provided!'); } - debugger + this.options = options; const columns = grid.columnList.toArray(); const colDefinitions = this.getColumns(columns, options); @@ -168,7 +169,7 @@ export abstract class IgxBaseExporter { this.mapHierarchicalGridColumns(rowIsland); } } else { - this._ownersMap.set('default', mapRecord); + this._ownersMap.set(DEFAULT_OWNER, mapRecord); } this.prepareData(grid, options); @@ -218,7 +219,7 @@ export abstract class IgxBaseExporter { indexOfLastPinnedColumn: -1 }; - this._ownersMap.set('default', mapRecord); + this._ownersMap.set(DEFAULT_OWNER, mapRecord); } for (const [key, mapRecord] of this._ownersMap) { @@ -236,7 +237,7 @@ export abstract class IgxBaseExporter { columnIndex: index, cancel: false, skipFormatter: false, - owner: key === 'default' ? undefined : key + owner: key === DEFAULT_OWNER ? undefined : key }; this.columnExporting.emit(columnExportArgs); @@ -277,7 +278,7 @@ export abstract class IgxBaseExporter { private exportRow(data: IExportRecord[], record: IExportRecord, index: number, isSpecialData: boolean) { if (!isSpecialData && record.type !== ExportRecordType.HeaderRecord) { const columns = record.owner === undefined ? - this._ownersMap.get('default').columns : + this._ownersMap.get(DEFAULT_OWNER).columns : this._ownersMap.get(record.owner).columns; record.data = columns.reduce((a, e) => { From cbf04af5723cc36751118171e2c79977f4bf9414 Mon Sep 17 00:00:00 2001 From: IBarakov Date: Thu, 1 Apr 2021 17:59:21 +0300 Subject: [PATCH 03/15] feat(hierarchical-grid): major refactoring --- .../src/lib/services/excel/excel-exporter.ts | 15 +++--- .../src/lib/services/excel/excel-files.ts | 54 +++++++++---------- .../src/lib/services/excel/worksheet-data.ts | 3 +- .../exporter-common/base-export-service.ts | 11 ++-- 4 files changed, 38 insertions(+), 45 deletions(-) diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts index e89d9bb5099..f404eff2d76 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts @@ -76,7 +76,6 @@ export class IgxExcelExporterService extends IgxBaseExporter { let rootKeys; let columnCount; let columnWidths; - let maxColumnSize; let indexOfLastPinnedColumn; if (typeof firstDataElement !== 'undefined') { @@ -91,22 +90,22 @@ export class IgxExcelExporterService extends IgxBaseExporter { } if (firstDataElement.type === ExportRecordType.HierarchicalGridRecord) { - maxColumnSize = data - .filter(rec => rec.type === ExportRecordType.HeaderRecord) - .map(a => a.data.length + a.level) + columnCount = data + .map(a => this._ownersMap.get(a.owner).columns.length + a.level) .sort((a,b) => b - a)[0]; + + rootKeys = this._ownersMap.get(firstDataElement.owner).columns.map(c => c.field); } else { const defaultOwner = this._ownersMap.get('default'); columnWidths = defaultOwner.columnWidths; indexOfLastPinnedColumn = defaultOwner.indexOfLastPinnedColumn; + columnCount = this._ownersMap.get('default').columns.length; + rootKeys = this._ownersMap.get('default').columns.filter(c => c.field); } - - rootKeys = Object.keys(firstDataElement.data); - columnCount = Object.keys(firstDataElement.data).length; } const worksheetData = - new WorksheetData(data, options, this._sort, columnCount, rootKeys, indexOfLastPinnedColumn, columnWidths, maxColumnSize); + new WorksheetData(data, options, this._sort, columnCount, rootKeys, indexOfLastPinnedColumn, columnWidths); this._xlsx = new (JSZip as any).default(); diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts index 0dbbacbbe3f..caa48c3aa66 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts @@ -62,7 +62,6 @@ export class WorksheetFile implements IExcelFile { private freezePane = ''; private rowHeight = ''; private globalCounter = 0; - private isHierarchicalGrid; public writeElement() {} @@ -88,22 +87,22 @@ export class WorksheetFile implements IExcelFile { this.dimension = 'A1'; done('', sheetData); } else { - this.isHierarchicalGrid = worksheetData.data[0].type === ExportRecordType.HierarchicalGridRecord; + const isHierarchicalGrid = worksheetData.data[0].type === ExportRecordType.HierarchicalGridRecord; const height = worksheetData.options.rowHeight; - const rowStyle = this.isHierarchicalGrid ? ' s="3"' : ''; + const rowStyle = isHierarchicalGrid ? ' s="3"' : ''; this.rowHeight = height ? ` ht="${height}" customHeight="1"` : ''; sheetData += ``; - for (let i = 0; i < worksheetData.columnCount; i++) { + for (let i = 0; i < worksheetData.rootKeys.length; i++) { const column = ExcelStrings.getExcelColumn(i) + 1; const value = dictionary.saveValue(worksheetData.keys[i], i, true); sheetData += `${value}`; } sheetData += ''; - if (!this.isHierarchicalGrid) { + if (!isHierarchicalGrid) { this.dimension = 'A1:' + ExcelStrings.getExcelColumn(worksheetData.columnCount - 1) + worksheetData.rowCount; cols += ''; @@ -133,7 +132,7 @@ export class WorksheetFile implements IExcelFile { ``; } } else { - cols += ``; + cols += ``; } this.processDataRecordsAsync(worksheetData, (rows) => { @@ -159,40 +158,32 @@ export class WorksheetFile implements IExcelFile { } private processRow(worksheetData: WorksheetData, i: number) { - const rowData = new Array(worksheetData.columnCount + 2); const record = worksheetData.data[i - 1]; - const sHidden = record.hidden ? ` hidden="1"` : ''; + + const isHierarchicalGrid = record.type === ExportRecordType.HeaderRecord || record.type === ExportRecordType.HierarchicalGridRecord; + const rowData = new Array(worksheetData.columnCount + 2); + const rowLevel = record.level; const outlineLevel = rowLevel > 0 ? ` outlineLevel="${rowLevel}"` : ''; - this.maxOutlineLevel = this.maxOutlineLevel < rowLevel ? rowLevel : this.maxOutlineLevel; - rowData[0] = ``; - - if (record.type === ExportRecordType.HeaderRecord) { - for (let j = 0; j < record.data.length; j++) { - const excelColumn = record.level + j; - const column = ExcelStrings.getExcelColumn(excelColumn) + (i + 1); - const value = worksheetData.dataDictionary.saveValue(record.data[j].header, i, true); - rowData[j + 1] = `${value}`; - } + const sHidden = record.hidden ? ` hidden="1"` : ''; - rowData[record.data.length + 1] = ''; - } else { - const keys = Object.keys(record.data); + rowData[0] = ``; - for (let j = 0; j < keys.length; j++) { - const col = j + (this.isHierarchicalGrid ? record.level : 0); + const keys = Object.keys(record.data); - const cellData = WorksheetFile.getCellData(worksheetData, i, col, keys[j], this.globalCounter); + for (let j = 0; j < keys.length; j++) { + const col = j + (isHierarchicalGrid ? rowLevel : 0); - rowData[j + 1] = cellData; - this.globalCounter++; - } + const cellData = WorksheetFile.getCellData(worksheetData, i, col, keys[j], this.globalCounter); - rowData[keys.length + 1] = ''; + rowData[j + 1] = cellData; + this.globalCounter++; } + rowData[keys.length + 1] = ''; + return rowData.join(''); } @@ -201,10 +192,13 @@ export class WorksheetFile implements IExcelFile { const dictionary = worksheetData.dataDictionary; const columnName = ExcelStrings.getExcelColumn(column) + (row + 1); const fullRow = worksheetData.data[row - 1]; + const isHeaderRecord = fullRow.type === ExportRecordType.HeaderRecord; const cellValue = worksheetData.isSpecialData ? fullRow.data : - fullRow.data[key]; + isHeaderRecord ? + fullRow.data[key].header : + fullRow.data[key]; if (cellValue === undefined || cellValue === null) { return ``; @@ -224,7 +218,7 @@ export class WorksheetFile implements IExcelFile { const type = isSavedAsString ? ` t="s"` : isSavedAsDate ? ` t="d"` : ''; - const format = isSavedAsString ? '' : isSavedAsDate ? ` s="2"` : ` s="1"`; + const format = isHeaderRecord ? ` s="3"` : isSavedAsString ? '' : isSavedAsDate ? ` s="2"` : ` s="1"`; return `${value}`; } diff --git a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts index ee3c7cf8b79..a78d8c6ecf9 100644 --- a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts +++ b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts @@ -15,8 +15,7 @@ export class WorksheetData { public columnCount: number, public rootKeys: string[], public indexOfLastPinnedColumn: number, - public columnWidths: number[], - public maxColumnSize: number) { + public columnWidths: number[]) { this.initializeData(); } 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 b4efdbb99a8..1ca1a5a9bea 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 @@ -348,30 +348,31 @@ export abstract class IgxBaseExporter { private addHierarchicalGridData(grid: IgxHierarchicalGridComponent, records: any[], level: number) { const childLayoutKeys = grid.childLayoutKeys; const childGrids = grid.hgridAPI.getChildGrids(true); + const columnFields = this._ownersMap.get(grid).columns.map(col => col.field); for(const entry of records) { const expansionStateVal = grid.expansionStates.has(entry) ? grid.expansionStates.get(entry) : false; const dataWithoutChildren = Object.keys(entry) - .filter(k => !childLayoutKeys.includes(k)) + .filter(k => columnFields.includes(k)) .reduce((obj, key) => { obj[key] = entry[key]; return obj; }, {}); - const firstRow: IExportRecord = { + const hierarchicalGridRecord: IExportRecord = { data: dataWithoutChildren, level, type: ExportRecordType.HierarchicalGridRecord, owner: grid, }; - this.flatRecords.push(firstRow); + this.flatRecords.push(hierarchicalGridRecord); for (const key of childLayoutKeys) { const island = grid.childLayoutList.filter(l => l.key === key)[0]; const keyRecordData = entry[key] || []; - + // filter sort island keyrecordadata this.getAllChildColumnsAndData(island, keyRecordData, expansionStateVal, childGrids); } } @@ -396,7 +397,7 @@ export abstract class IgxBaseExporter { const exportRecord: IExportRecord = { data: rec, level: island.level, - type: ExportRecordType.DataRecord, + type: ExportRecordType.HierarchicalGridRecord, owner: island, hidden: !expansionStateVal }; From a2f69d18f762be6b97e8bcb1499661039307ab48 Mon Sep 17 00:00:00 2001 From: IBarakov Date: Tue, 6 Apr 2021 17:12:47 +0300 Subject: [PATCH 04/15] feat(hierarchical-grid): major base-export refactoring --- .../src/lib/services/excel/worksheet-data.ts | 2 +- .../exporter-common/base-export-service.ts | 171 +++++++++++++----- 2 files changed, 122 insertions(+), 51 deletions(-) diff --git a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts index a78d8c6ecf9..524db219271 100644 --- a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts +++ b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts @@ -47,7 +47,7 @@ export class WorksheetData { if (!this._data || this._data.length === 0) { return; } - debugger + const actualData = this._data.filter(item => item.type !== ExportRecordType.HeaderRecord).map(item => item.data); if (this._data[0].type === ExportRecordType.HierarchicalGridRecord) { 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 1ca1a5a9bea..25de4e6a6a3 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 @@ -148,8 +148,9 @@ export abstract class IgxBaseExporter { } this.options = options; + const columns = grid.columnList.toArray(); - const colDefinitions = this.getColumns(columns, options); + const colDefinitions = this.getColumns(columns); const mapRecord: IMapRecord = { columns: colDefinitions.colList, @@ -172,8 +173,8 @@ export abstract class IgxBaseExporter { this._ownersMap.set(DEFAULT_OWNER, mapRecord); } - this.prepareData(grid, options); - this.exportGridRecordsData(this.flatRecords, options); + this.prepareData(grid); + this.exportGridRecordsData(this.flatRecords); } /** @@ -189,6 +190,8 @@ export abstract class IgxBaseExporter { throw Error('No options provided!'); } + this.options = options; + const records = data.map(d => { const record: IExportRecord = { data: d, @@ -199,14 +202,10 @@ export abstract class IgxBaseExporter { return record; }); - this.exportGridRecordsData(records, options); + this.exportGridRecordsData(records); } - private exportGridRecordsData(records: IExportRecord[], options: IgxExporterOptionsBase) { - if (options === undefined || options === null) { - throw Error('No options provided!'); - } - + private exportGridRecordsData(records: IExportRecord[]) { if (this._ownersMap.size === 0) { const recordsData = records.map(r => r.data); const keys = ExportUtilities.getKeysFromData(recordsData); @@ -270,7 +269,7 @@ export abstract class IgxBaseExporter { const row = records[i]; this.exportRow(dataToExport, row, i, isSpecialData); }, () => { - this.exportDataImplementation(dataToExport, options); + this.exportDataImplementation(dataToExport, this.options); this.resetDefaults(); }); } @@ -316,7 +315,7 @@ export abstract class IgxBaseExporter { } } - private prepareData(grid: IgxGridBaseDirective, options: IgxExporterOptionsBase) { + private prepareData(grid: IgxGridBaseDirective) { this.flatRecords = []; const tagName = grid.nativeElement.tagName.toLowerCase(); @@ -327,25 +326,52 @@ export abstract class IgxBaseExporter { grid.sortingExpressions.length > 0; if (tagName === 'igx-hierarchical-grid') { - this.prepareHierarchicalGridData(grid as IgxHierarchicalGridComponent); + this.prepareHierarchicalGridData(grid as IgxHierarchicalGridComponent, hasFiltering, hasSorting); } else { if (tagName === 'igx-grid') { - this.prepareGridData(grid as IgxGridComponent, options, hasFiltering, hasSorting); + this.prepareGridData(grid as IgxGridComponent, hasFiltering, hasSorting); } if (tagName === 'igx-tree-grid') { - this.prepareTreeGridData(grid as IgxTreeGridComponent, options, hasFiltering, hasSorting); + this.prepareTreeGridData(grid as IgxTreeGridComponent, hasFiltering, hasSorting); } } } - private prepareHierarchicalGridData(grid: IgxHierarchicalGridComponent, level: number = 0) { - //options: IgxExporterOptionsBase, hasFiltering: boolean, hasSorting: boolean - const records = grid.data; - this.addHierarchicalGridData(grid, records, level); + private prepareHierarchicalGridData(grid: IgxHierarchicalGridComponent, hasFiltering: boolean, hasSorting: boolean) { + + const skipOperations = + (!hasFiltering || !this.options.ignoreFiltering) && + (!hasSorting || !this.options.ignoreSorting); + + if (skipOperations) { + const data = grid.filteredSortedData; + this.addHierarchicalGridData(grid, data); + } else { + let data = grid.data; + + if (hasFiltering && !this.options.ignoreFiltering) { + const filteringState: IFilteringState = { + expressionsTree: grid.filteringExpressionsTree, + advancedExpressionsTree: grid.advancedFilteringExpressionsTree, + }; + + filteringState.strategy = grid.filterStrategy; + + data = DataUtil.filter(data, filteringState, grid); + } + + if (hasSorting && !this.options.ignoreSorting) { + this._sort = cloneValue(grid.sortingExpressions[0]); + + data = DataUtil.treeGridSort(data, grid.sortingExpressions, grid.sortStrategy); + } + + this.addHierarchicalGridData(grid, data); + } } - private addHierarchicalGridData(grid: IgxHierarchicalGridComponent, records: any[], level: number) { + private addHierarchicalGridData(grid: IgxHierarchicalGridComponent, records: any[]) { const childLayoutKeys = grid.childLayoutKeys; const childGrids = grid.hgridAPI.getChildGrids(true); const columnFields = this._ownersMap.get(grid).columns.map(col => col.field); @@ -362,7 +388,7 @@ export abstract class IgxBaseExporter { const hierarchicalGridRecord: IExportRecord = { data: dataWithoutChildren, - level, + level: 0, type: ExportRecordType.HierarchicalGridRecord, owner: grid, }; @@ -371,16 +397,58 @@ export abstract class IgxBaseExporter { for (const key of childLayoutKeys) { const island = grid.childLayoutList.filter(l => l.key === key)[0]; - const keyRecordData = entry[key] || []; - // filter sort island keyrecordadata - this.getAllChildColumnsAndData(island, keyRecordData, expansionStateVal, childGrids); + const islandGrid = childGrids.filter(g => g.key === island.key && g.data === entry[key])[0]; + + const keyRecordData = this.prepareIslandData(islandGrid, entry[key]) || []; + + this.getAllChildColumnsAndData(island, keyRecordData, expansionStateVal, childGrids, grid); } } } - private getAllChildColumnsAndData(island: IgxRowIslandComponent, childData: any, expansionStateVal: boolean, childGrids: any) { + private prepareIslandData(islandGrid: IgxHierarchicalGridComponent, data: any): any { + if (islandGrid !== undefined) { + const hasFiltering = (islandGrid.filteringExpressionsTree && + islandGrid.filteringExpressionsTree.filteringOperands.length > 0) || + (islandGrid.advancedFilteringExpressionsTree && + islandGrid.advancedFilteringExpressionsTree.filteringOperands.length > 0); + + const hasSorting = islandGrid.sortingExpressions && + islandGrid.sortingExpressions.length > 0; + + const skipOperations = + (!hasFiltering || !this.options.ignoreFiltering) && + (!hasSorting || !this.options.ignoreSorting); + + if (skipOperations) { + data = islandGrid.filteredSortedData; + } else { + if (hasFiltering && !this.options.ignoreFiltering) { + const filteringState: IFilteringState = { + expressionsTree: islandGrid.filteringExpressionsTree, + advancedExpressionsTree: islandGrid.advancedFilteringExpressionsTree, + }; + + filteringState.strategy = islandGrid.filterStrategy; + + data = DataUtil.filter(data, filteringState, islandGrid); + } + + if (hasSorting && !this.options.ignoreSorting) { + this._sort = cloneValue(islandGrid.sortingExpressions[0]); + + data = DataUtil.treeGridSort(data, islandGrid.sortingExpressions, islandGrid.sortStrategy); + } + } + } + + return data; + } + + private getAllChildColumnsAndData(island: IgxRowIslandComponent, + childData: any, expansionStateVal: boolean, childGrids: any, grid: any) { const islandColumnList = island.childColumns.toArray(); - const modifiedColumns = this.getColumns(islandColumnList, this.options); + const modifiedColumns = this.getColumns(islandColumnList); const headerRecord: IExportRecord = { data: modifiedColumns.colList, @@ -405,23 +473,26 @@ export abstract class IgxBaseExporter { this.flatRecords.push(exportRecord); if (island.children.length > 0) { - for (const childIsland of island.children) { - const islandGrid = childGrids.filter(g => g.key === island.key); + const islandGrid = childGrids.filter(g => g.key === island.key && g.data.some(d => d === rec))[0]; - const islandExpansionStateVal = islandGrid.length === 0 ? - false : - islandGrid[0].expansionStates.has(rec) ? - islandGrid[0].expansionStates.get(rec) : // TODO get grid by row - false; + const islandExpansionStateVal = islandGrid === undefined ? + false : + islandGrid.expansionStates.has(rec) ? + islandGrid.expansionStates.get(rec) : + false; + + for (const childIsland of island.children) { + const childIslandGrid = childGrids.filter(g => g.key === childIsland.key && g.data === rec[childIsland.key])[0]; + const keyRecordData = this.prepareIslandData(childIslandGrid, rec[childIsland.key]) || []; - this.getAllChildColumnsAndData(childIsland, rec[childIsland.key], islandExpansionStateVal, childGrids); + this.getAllChildColumnsAndData(childIsland, keyRecordData, islandExpansionStateVal, childGrids, grid); } } } } } - private prepareGridData(grid: IgxGridComponent, options: IgxExporterOptionsBase, hasFiltering: boolean, hasSorting: boolean) { + private prepareGridData(grid: IgxGridComponent, hasFiltering: boolean, hasSorting: boolean) { const groupedGridGroupingState: IGroupingState = { expressions: grid.groupingExpressions, expansion: grid.groupingExpansionState, @@ -432,9 +503,9 @@ export abstract class IgxBaseExporter { grid.groupingExpressions.length > 0; const skipOperations = - (!hasFiltering || !options.ignoreFiltering) && - (!hasSorting || !options.ignoreSorting) && - (!hasGrouping || !options.ignoreGrouping); + (!hasFiltering || !this.options.ignoreFiltering) && + (!hasSorting || !this.options.ignoreSorting) && + (!hasGrouping || !this.options.ignoreGrouping); if (skipOperations) { if (hasGrouping) { @@ -445,7 +516,7 @@ export abstract class IgxBaseExporter { } else { let gridData = grid.data; - if (hasFiltering && !options.ignoreFiltering) { + if (hasFiltering && !this.options.ignoreFiltering) { const filteringState: IFilteringState = { expressionsTree: grid.filteringExpressionsTree, advancedExpressionsTree: grid.advancedFilteringExpressionsTree, @@ -454,7 +525,7 @@ export abstract class IgxBaseExporter { gridData = DataUtil.filter(gridData, filteringState, grid); } - if (hasSorting && !options.ignoreSorting) { + if (hasSorting && !this.options.ignoreSorting) { // TODO: We should drop support for this since in a grouped grid it doesn't make sense // this._sort = !isGroupedGrid ? // cloneValue(grid.sortingExpressions[0]) : @@ -465,13 +536,13 @@ export abstract class IgxBaseExporter { gridData = DataUtil.sort(gridData, grid.sortingExpressions, grid.sortStrategy, grid); } - if (hasGrouping && !options.ignoreGrouping) { + if (hasGrouping && !this.options.ignoreGrouping) { const groupsRecords = []; DataUtil.group(cloneArray(gridData), groupedGridGroupingState, grid, groupsRecords); gridData = groupsRecords; } - if (hasGrouping && !options.ignoreGrouping) { + if (hasGrouping && !this.options.ignoreGrouping) { this.addGroupedData(grid, gridData, groupedGridGroupingState); } else { this.addFlatData(gridData); @@ -479,17 +550,17 @@ export abstract class IgxBaseExporter { } } - private prepareTreeGridData(grid: IgxTreeGridComponent, options: IgxExporterOptionsBase, hasFiltering: boolean, hasSorting: boolean) { + private prepareTreeGridData(grid: IgxTreeGridComponent, hasFiltering: boolean, hasSorting: boolean) { const skipOperations = - (!hasFiltering || !options.ignoreFiltering) && - (!hasSorting || !options.ignoreSorting); + (!hasFiltering || !this.options.ignoreFiltering) && + (!hasSorting || !this.options.ignoreSorting); if (skipOperations) { this.addTreeGridData(grid.processedRootRecords); } else { let gridData = grid.rootRecords; - if (hasFiltering && !options.ignoreFiltering) { + if (hasFiltering && !this.options.ignoreFiltering) { const filteringState: IFilteringState = { expressionsTree: grid.filteringExpressionsTree, advancedExpressionsTree: grid.advancedFilteringExpressionsTree, @@ -501,7 +572,7 @@ export abstract class IgxBaseExporter { .filter(gridData, filteringState.expressionsTree, filteringState.advancedExpressionsTree); } - if (hasSorting && !options.ignoreSorting) { + if (hasSorting && !this.options.ignoreSorting) { this._sort = cloneValue(grid.sortingExpressions[0]); gridData = DataUtil.treeGridSort(gridData, grid.sortingExpressions, grid.sortStrategy); @@ -604,7 +675,7 @@ export abstract class IgxBaseExporter { } } - private getColumns(columns: any, options: IgxExporterOptionsBase): any { + private getColumns(columns: any): any { const colList = new Array(columns.length); const colWidthList = new Array(columns.filter(c => !c.hidden).length); const hiddenColumns = []; @@ -613,8 +684,8 @@ export abstract class IgxBaseExporter { columns.forEach((column, i) => { const columnHeader = !ExportUtilities.isNullOrWhitespaces(column.header) ? column.header : column.field; - const exportColumn = !column.hidden || options.ignoreColumnsVisibility; - const index = options.ignoreColumnsOrder || options.ignoreColumnsVisibility ? column.index : column.visibleIndex; + const exportColumn = !column.hidden || this.options.ignoreColumnsVisibility; + const index = this.options.ignoreColumnsOrder || this.options.ignoreColumnsVisibility ? column.index : column.visibleIndex; const columnWidth = Number(column.width?.slice(0, -2)) || DEFAULT_COLUMN_WIDTH; const columnInfo = { @@ -655,7 +726,7 @@ export abstract class IgxBaseExporter { private mapHierarchicalGridColumns(island: IgxRowIslandComponent) { const islandColumnList = island.childColumns.toArray(); - const colDefinitions = this.getColumns(islandColumnList, this.options); + const colDefinitions = this.getColumns(islandColumnList); const mapRecord: IMapRecord = { columns: colDefinitions.colList, From 0858599c1e52e4557673a54d87ba2b2178a0c665 Mon Sep 17 00:00:00 2001 From: IBarakov Date: Tue, 13 Apr 2021 11:56:34 +0300 Subject: [PATCH 05/15] feat(hierarchical-grid): address PR comments --- .../services/csv/char-separated-value-data.ts | 4 +- .../src/lib/services/excel/excel-exporter.ts | 8 +- .../src/lib/services/excel/excel-files.ts | 22 ++- .../src/lib/services/excel/excel-strings.ts | 10 +- .../excel/worksheet-data-dictionary.ts | 11 +- .../src/lib/services/excel/worksheet-data.ts | 10 +- .../exporter-common/base-export-service.ts | 150 +++++++++--------- .../exporter-common/export-utilities.ts | 11 +- 8 files changed, 107 insertions(+), 119 deletions(-) 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 5a3f3e356b5..f4fb3888e62 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 @@ -32,7 +32,7 @@ export class CharSeparatedValueData { return ''; } - this._isSpecialData = ExportUtilities.isSpecialData(this._data); + this._isSpecialData = ExportUtilities.isSpecialData(this._data[0]); this._escapeCharacters.push(this._delimiter); this._headerRecord = this.processHeaderRecord(keys); @@ -52,7 +52,7 @@ export class CharSeparatedValueData { done(''); } - this._isSpecialData = ExportUtilities.isSpecialData(this._data); + this._isSpecialData = ExportUtilities.isSpecialData(this._data[0]); this._escapeCharacters.push(this._delimiter); this._headerRecord = this.processHeaderRecord(keys); diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts index f404eff2d76..0699f642807 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts @@ -5,7 +5,7 @@ import { ExcelElementsFactory } from './excel-elements-factory'; import { ExcelFolderTypes } from './excel-enums'; import { IgxExcelExporterOptions } from './excel-exporter-options'; import { IExcelFolder } from './excel-interfaces'; -import { ExportRecordType, IExportRecord, IgxBaseExporter } from '../exporter-common/base-export-service'; +import { ExportRecordType, IExportRecord, IgxBaseExporter, DEFAULT_OWNER } from '../exporter-common/base-export-service'; import { ExportUtilities } from '../exporter-common/export-utilities'; import { WorksheetData } from './worksheet-data'; import { IBaseEventArgs } from '../../core/utils'; @@ -96,11 +96,11 @@ export class IgxExcelExporterService extends IgxBaseExporter { rootKeys = this._ownersMap.get(firstDataElement.owner).columns.map(c => c.field); } else { - const defaultOwner = this._ownersMap.get('default'); + const defaultOwner = this._ownersMap.get(DEFAULT_OWNER); columnWidths = defaultOwner.columnWidths; indexOfLastPinnedColumn = defaultOwner.indexOfLastPinnedColumn; - columnCount = this._ownersMap.get('default').columns.length; - rootKeys = this._ownersMap.get('default').columns.filter(c => c.field); + columnCount = defaultOwner.columns.length; + rootKeys = defaultOwner.columns.map(c => c.field); } } diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts index caa48c3aa66..0f2bf67820d 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts @@ -61,7 +61,6 @@ export class WorksheetFile implements IExcelFile { private dimension = ''; private freezePane = ''; private rowHeight = ''; - private globalCounter = 0; public writeElement() {} @@ -69,9 +68,10 @@ export class WorksheetFile implements IExcelFile { return new Promise(resolve => { this.prepareDataAsync(worksheetData, (cols, rows) => { const hasTable = !worksheetData.isEmpty && worksheetData.options.exportAsTable; + const isHierarchicalGrid = worksheetData.data[0]?.type === ExportRecordType.HierarchicalGridRecord; folder.file('sheet1.xml', ExcelStrings.getSheetXML( - this.dimension, this.freezePane, cols, rows, hasTable, this.maxOutlineLevel)); + this.dimension, this.freezePane, cols, rows, hasTable, this.maxOutlineLevel, isHierarchicalGrid)); resolve(); }); }); @@ -97,7 +97,7 @@ export class WorksheetFile implements IExcelFile { for (let i = 0; i < worksheetData.rootKeys.length; i++) { const column = ExcelStrings.getExcelColumn(i) + 1; - const value = dictionary.saveValue(worksheetData.keys[i], i, true); + const value = dictionary.saveValue(worksheetData.rootKeys[i], true); sheetData += `${value}`; } sheetData += ''; @@ -132,7 +132,8 @@ export class WorksheetFile implements IExcelFile { ``; } } else { - cols += ``; + const columnWidth = worksheetData.options.columnWidth ? worksheetData.options.columnWidth : 20; + cols += ``; } this.processDataRecordsAsync(worksheetData, (rows) => { @@ -176,10 +177,9 @@ export class WorksheetFile implements IExcelFile { for (let j = 0; j < keys.length; j++) { const col = j + (isHierarchicalGrid ? rowLevel : 0); - const cellData = WorksheetFile.getCellData(worksheetData, i, col, keys[j], this.globalCounter); + const cellData = WorksheetFile.getCellData(worksheetData, i, col, keys[j]); rowData[j + 1] = cellData; - this.globalCounter++; } rowData[keys.length + 1] = ''; @@ -188,7 +188,7 @@ export class WorksheetFile implements IExcelFile { } /* eslint-disable @typescript-eslint/member-ordering */ - private static getCellData(worksheetData: WorksheetData, row: number, column: number, key: string, globalCounter: number): string { + private static getCellData(worksheetData: WorksheetData, row: number, column: number, key: string): string { const dictionary = worksheetData.dataDictionary; const columnName = ExcelStrings.getExcelColumn(column) + (row + 1); const fullRow = worksheetData.data[row - 1]; @@ -196,14 +196,12 @@ export class WorksheetFile implements IExcelFile { const cellValue = worksheetData.isSpecialData ? fullRow.data : - isHeaderRecord ? - fullRow.data[key].header : - fullRow.data[key]; + fullRow.data[key]; if (cellValue === undefined || cellValue === null) { return ``; } else { - const savedValue = dictionary.saveValue(cellValue, globalCounter, false); + const savedValue = dictionary.saveValue(cellValue, isHeaderRecord); const isSavedAsString = savedValue !== -1; const isSavedAsDate = !isSavedAsString && cellValue instanceof Date; @@ -286,7 +284,7 @@ export class TablesFile implements IExcelFile { const columnCount = worksheetData.columnCount; const lastColumn = ExcelStrings.getExcelColumn(columnCount - 1) + worksheetData.rowCount; const dimension = 'A1:' + lastColumn; - const values = worksheetData.keys; + const values = worksheetData.rootKeys; let sortString = ''; let tableColumns = ''; diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts b/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts index d33bdc42522..f6db23215c8 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts @@ -26,7 +26,7 @@ export class ExcelStrings { const fillsCount = isHierarchicalGrid ? 3 : 2; const additionalFill = fillsCount === 3 ? - ' ' : + '' : ''; const additionalFont = fontsCount === 2 ? @@ -72,20 +72,20 @@ export class ExcelStrings { return retVal; } - public static getSheetXML(dimension: string, freezePane: string, cols: string, sheetData: string, hasTable: boolean, outlineLevel = 0): string { + public static getSheetXML(dimension: string, freezePane: string, cols: string, sheetData: string, hasTable: boolean, outlineLevel = 0, isHierarchicalGrid: boolean): string { const hasOutline = outlineLevel > 0; const tableParts = hasTable ? '' : ''; const sheetOutlineProp = hasOutline ? '' : ''; const sOutlineLevel = hasOutline ? `outlineLevelRow="${outlineLevel}"` : ''; + const dimensions = isHierarchicalGrid ? '' : ``; + // return ExcelStrings.XML_STRING + // '' + freezePane + '' + cols + sheetData + '' + tableParts + ''; - - // TODO FIX ! - //below sheetoutlineprop - return `${ExcelStrings.XML_STRING} ${sheetOutlineProp} +${dimensions} ${freezePane} ${cols} diff --git a/projects/igniteui-angular/src/lib/services/excel/worksheet-data-dictionary.ts b/projects/igniteui-angular/src/lib/services/excel/worksheet-data-dictionary.ts index e83b03452d6..1146cc85186 100644 --- a/projects/igniteui-angular/src/lib/services/excel/worksheet-data-dictionary.ts +++ b/projects/igniteui-angular/src/lib/services/excel/worksheet-data-dictionary.ts @@ -20,8 +20,6 @@ export class WorksheetDataDictionary { private _columnWidths: number[]; private _context: any; - private _columnTypeInfo: boolean[]; - constructor(columnCount: number, columnWidth: number, columnWidthsList: number[]) { this._dictionary = {}; this._widthsDictionary = {}; @@ -29,7 +27,6 @@ export class WorksheetDataDictionary { this.dirtyKeyCollections(); this._columnWidths = new Array(columnCount); - this._columnTypeInfo = new Array(columnCount); if (columnWidth) { this._columnWidths.fill(columnWidth); @@ -44,14 +41,10 @@ export class WorksheetDataDictionary { return this._columnWidths; } - public saveValue(value: any, column: number, isHeader: boolean): number { - if (this._columnTypeInfo[column] === undefined && isHeader === false) { - this._columnTypeInfo[column] = typeof value !== 'number' && value !== Number(value) && !Number.isFinite(value); - } - + public saveValue(value: any, isHeader: boolean): number { let sanitizedValue = ''; const isDate = value instanceof Date; - const isSavedAsString = (this._columnTypeInfo[column] || isHeader) && !isDate; + const isSavedAsString = isHeader || (typeof value !== 'number' && value !== Number(value) && !Number.isFinite(value) && !isDate); if (isSavedAsString) { sanitizedValue = this.sanitizeValue(value); diff --git a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts index 524db219271..3854db790fa 100644 --- a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts +++ b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts @@ -1,4 +1,4 @@ -import { ExportRecordType, IExportRecord, IMapRecord } from '../exporter-common/base-export-service'; +import { ExportRecordType, IExportRecord } from '../exporter-common/base-export-service'; import { ExportUtilities } from '../exporter-common/export-utilities'; import { IgxExcelExporterOptions } from './excel-exporter-options'; import { WorksheetDataDictionary } from './worksheet-data-dictionary'; @@ -31,10 +31,6 @@ export class WorksheetData { return !this.rowCount || !this.columnCount; } - public get keys(): string[] { - return this.rootKeys; - } - public get isSpecialData(): boolean { return this._isSpecialData; } @@ -48,13 +44,11 @@ export class WorksheetData { return; } - const actualData = this._data.filter(item => item.type !== ExportRecordType.HeaderRecord).map(item => item.data); - if (this._data[0].type === ExportRecordType.HierarchicalGridRecord) { this.options.exportAsTable = false; } - this._isSpecialData = ExportUtilities.isSpecialData(actualData); + this._isSpecialData = ExportUtilities.isSpecialData(this._data[0]); this._rowCount = this._data.length + 1; this._dataDictionary = new WorksheetDataDictionary(this.columnCount, this.options.columnWidth, this.columnWidths); } 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 25de4e6a6a3..2c795957cdf 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 @@ -1,6 +1,6 @@ import { EventEmitter } from '@angular/core'; import { cloneArray, cloneValue, IBaseEventArgs, resolveNestedPath, yieldingLoop } from '../../core/utils'; -import { DataUtil } from '../../data-operations/data-util'; +import { DataType, DataUtil } from '../../data-operations/data-util'; import { ExportUtilities } from './export-utilities'; import { IgxExporterOptionsBase } from './exporter-options-base'; import { ITreeGridRecord } from '../../grids/tree-grid/tree-grid.interfaces'; @@ -9,13 +9,14 @@ import { IGroupingState } from '../../data-operations/groupby-state.interface'; import { getHierarchy, isHierarchyMatch } from '../../data-operations/operations'; import { IGroupByExpandState } from '../../data-operations/groupby-expand-state.interface'; import { IFilteringState } from '../../data-operations/filtering-state.interface'; -import { IgxGridBaseDirective } from '../../grids/public_api'; +import { IgxColumnComponent, IgxGridBaseDirective } from '../../grids/public_api'; import { IgxTreeGridComponent } from '../../grids/tree-grid/public_api'; import { IgxGridComponent } from '../../grids/grid/public_api'; import { DatePipe } from '@angular/common'; import { IGroupByRecord } from '../../data-operations/groupby-record.interface'; import { IgxHierarchicalGridComponent } from '../../grids/hierarchical-grid/hierarchical-grid.component'; -import { IgxRowIslandComponent } from './../../../../../../dist/igniteui-angular/esm2015/lib/grids/hierarchical-grid/row-island.component'; +import { IgxRowIslandComponent } from '../../grids/hierarchical-grid/row-island.component'; +import { IPathSegment } from './../../grids/hierarchical-grid/hierarchical-grid-base.directive'; export enum ExportRecordType { GroupedRecord = 1, @@ -33,10 +34,19 @@ export interface IExportRecord { hidden?: boolean; } -export interface IMapRecord { - indexOfLastPinnedColumn: number; +export interface IColumnList { + columns: IColumnInfo[]; columnWidths: number[]; - columns: any; + indexOfLastPinnedColumn: number; +} + +export interface IColumnInfo { + header: string; + field: string; + skip: boolean; + dataType?: DataType; + skipFormatter?: boolean; + formatter?: any; } /** @@ -97,7 +107,7 @@ export interface IColumnExportingEventArgs extends IBaseEventArgs { skipFormatter: boolean; } -const DEFAULT_OWNER = 'default'; +export const DEFAULT_OWNER = 'default'; const DEFAULT_COLUMN_WIDTH = 8.43; export abstract class IgxBaseExporter { @@ -129,7 +139,7 @@ export abstract class IgxBaseExporter { public columnExporting = new EventEmitter(); protected _sort = null; - protected _ownersMap: Map = new Map(); + protected _ownersMap: Map = new Map(); private flatRecords: IExportRecord[] = []; private options: IgxExporterOptionsBase; @@ -150,27 +160,20 @@ export abstract class IgxBaseExporter { this.options = options; const columns = grid.columnList.toArray(); - const colDefinitions = this.getColumns(columns); - - const mapRecord: IMapRecord = { - columns: colDefinitions.colList, - columnWidths: colDefinitions.colWidthList, - indexOfLastPinnedColumn: colDefinitions.indexOfLastPinnedColumn, - }; + const columnList = this.getColumns(columns); const tagName = grid.nativeElement.tagName.toLowerCase(); if (tagName === 'igx-hierarchical-grid') { - this._ownersMap.set(grid, mapRecord); + this._ownersMap.set(grid, columnList); - const keys = (grid as IgxHierarchicalGridComponent).childLayoutKeys; + const childLayoutList = (grid as IgxHierarchicalGridComponent).childLayoutList; - for (const key of keys) { - const rowIsland = grid.childLayoutList.filter(l => l.key === key)[0]; - this.mapHierarchicalGridColumns(rowIsland); + for (const island of childLayoutList) { + this.mapHierarchicalGridColumns(island); } } else { - this._ownersMap.set(DEFAULT_OWNER, mapRecord); + this._ownersMap.set(DEFAULT_OWNER, columnList); } this.prepareData(grid); @@ -212,7 +215,7 @@ export abstract class IgxBaseExporter { const columns = keys.map((k) => ({ header: k, field: k, skip: false })); const columnWidths = new Array(keys.length).fill(DEFAULT_COLUMN_WIDTH); - const mapRecord: IMapRecord = { + const mapRecord: IColumnList = { columns, columnWidths, indexOfLastPinnedColumn: -1 @@ -228,7 +231,7 @@ export abstract class IgxBaseExporter { mapRecord.columns.forEach((column, index) => { if (!column.skip) { - const columnExportArgs = { + const columnExportArgs: IColumnExportingEventArgs = { header: !ExportUtilities.isNullOrWhitespaces(column.header) ? column.header : 'Column' + columnsWithoutHeaderCount++, @@ -262,7 +265,7 @@ export abstract class IgxBaseExporter { } const dataToExport = new Array(); - const actualData = records.map(r => r.data); + const actualData = records[0]?.data; const isSpecialData = ExportUtilities.isSpecialData(actualData); yieldingLoop(records.length, 100, (i) => { @@ -325,15 +328,18 @@ export abstract class IgxBaseExporter { const hasSorting = grid.sortingExpressions && grid.sortingExpressions.length > 0; - if (tagName === 'igx-hierarchical-grid') { - this.prepareHierarchicalGridData(grid as IgxHierarchicalGridComponent, hasFiltering, hasSorting); - } else { - if (tagName === 'igx-grid') { - this.prepareGridData(grid as IgxGridComponent, hasFiltering, hasSorting); + switch(tagName) { + case 'igx-hierarchical-grid': { + this.prepareHierarchicalGridData(grid as IgxHierarchicalGridComponent, hasFiltering, hasSorting); + break; } - - if (tagName === 'igx-tree-grid') { + case 'igx-tree-grid': { this.prepareTreeGridData(grid as IgxTreeGridComponent, hasFiltering, hasSorting); + break; + } + default: { + this.prepareGridData(grid as IgxGridComponent, hasFiltering, hasSorting); + break; } } } @@ -354,17 +360,16 @@ export abstract class IgxBaseExporter { const filteringState: IFilteringState = { expressionsTree: grid.filteringExpressionsTree, advancedExpressionsTree: grid.advancedFilteringExpressionsTree, + strategy: grid.filterStrategy }; - filteringState.strategy = grid.filterStrategy; - data = DataUtil.filter(data, filteringState, grid); } if (hasSorting && !this.options.ignoreSorting) { this._sort = cloneValue(grid.sortingExpressions[0]); - data = DataUtil.treeGridSort(data, grid.sortingExpressions, grid.sortStrategy); + data = DataUtil.sort(data, grid.sortingExpressions, grid.sortStrategy, grid); } this.addHierarchicalGridData(grid, data); @@ -372,8 +377,7 @@ export abstract class IgxBaseExporter { } private addHierarchicalGridData(grid: IgxHierarchicalGridComponent, records: any[]) { - const childLayoutKeys = grid.childLayoutKeys; - const childGrids = grid.hgridAPI.getChildGrids(true); + const childLayoutList = grid.childLayoutList; const columnFields = this._ownersMap.get(grid).columns.map(col => col.field); for(const entry of records) { @@ -395,18 +399,21 @@ export abstract class IgxBaseExporter { this.flatRecords.push(hierarchicalGridRecord); - for (const key of childLayoutKeys) { - const island = grid.childLayoutList.filter(l => l.key === key)[0]; - const islandGrid = childGrids.filter(g => g.key === island.key && g.data === entry[key])[0]; + for (const island of childLayoutList) { + const path: IPathSegment = { + rowID: island.primaryKey ? entry[island.primaryKey] : entry, + rowIslandKey: island.key + }; - const keyRecordData = this.prepareIslandData(islandGrid, entry[key]) || []; + const islandGrid = grid?.hgridAPI.getChildGrid([path]); + const keyRecordData = this.prepareIslandData(islandGrid, entry[island.key]) || []; - this.getAllChildColumnsAndData(island, keyRecordData, expansionStateVal, childGrids, grid); + this.getAllChildColumnsAndData(island, keyRecordData, expansionStateVal, islandGrid); } } } - private prepareIslandData(islandGrid: IgxHierarchicalGridComponent, data: any): any { + private prepareIslandData(islandGrid: IgxHierarchicalGridComponent, data: any[]): any[] { if (islandGrid !== undefined) { const hasFiltering = (islandGrid.filteringExpressionsTree && islandGrid.filteringExpressionsTree.filteringOperands.length > 0) || @@ -427,17 +434,16 @@ export abstract class IgxBaseExporter { const filteringState: IFilteringState = { expressionsTree: islandGrid.filteringExpressionsTree, advancedExpressionsTree: islandGrid.advancedFilteringExpressionsTree, + strategy: islandGrid.filterStrategy }; - filteringState.strategy = islandGrid.filterStrategy; - data = DataUtil.filter(data, filteringState, islandGrid); } if (hasSorting && !this.options.ignoreSorting) { this._sort = cloneValue(islandGrid.sortingExpressions[0]); - data = DataUtil.treeGridSort(data, islandGrid.sortingExpressions, islandGrid.sortStrategy); + data = DataUtil.sort(data, islandGrid.sortingExpressions, islandGrid.sortStrategy, islandGrid); } } } @@ -446,12 +452,13 @@ export abstract class IgxBaseExporter { } private getAllChildColumnsAndData(island: IgxRowIslandComponent, - childData: any, expansionStateVal: boolean, childGrids: any, grid: any) { + childData: any[], expansionStateVal: boolean, grid: IgxHierarchicalGridComponent) { const islandColumnList = island.childColumns.toArray(); - const modifiedColumns = this.getColumns(islandColumnList); + const columnList = this.getColumns(islandColumnList); + const columnHeader = columnList.columns.map(col => col.header ? col.header : col.field); const headerRecord: IExportRecord = { - data: modifiedColumns.colList, + data: columnHeader, level: island.level, type: ExportRecordType.HeaderRecord, owner: island, @@ -473,19 +480,22 @@ export abstract class IgxBaseExporter { this.flatRecords.push(exportRecord); if (island.children.length > 0) { - const islandGrid = childGrids.filter(g => g.key === island.key && g.data.some(d => d === rec))[0]; - - const islandExpansionStateVal = islandGrid === undefined ? + const islandExpansionStateVal = grid === undefined ? false : - islandGrid.expansionStates.has(rec) ? - islandGrid.expansionStates.get(rec) : + grid.expansionStates.has(rec) ? + grid.expansionStates.get(rec) : false; for (const childIsland of island.children) { - const childIslandGrid = childGrids.filter(g => g.key === childIsland.key && g.data === rec[childIsland.key])[0]; + const path: IPathSegment = { + rowID: childIsland.primaryKey ? rec[childIsland.primaryKey] : rec, + rowIslandKey: childIsland.key + }; + + const childIslandGrid = grid?.hgridAPI.getChildGrid([path]); const keyRecordData = this.prepareIslandData(childIslandGrid, rec[childIsland.key]) || []; - this.getAllChildColumnsAndData(childIsland, keyRecordData, islandExpansionStateVal, childGrids, grid); + this.getAllChildColumnsAndData(childIsland, keyRecordData, islandExpansionStateVal, childIslandGrid); } } } @@ -520,8 +530,9 @@ export abstract class IgxBaseExporter { const filteringState: IFilteringState = { expressionsTree: grid.filteringExpressionsTree, advancedExpressionsTree: grid.advancedFilteringExpressionsTree, + strategy: grid.filterStrategy }; - filteringState.strategy = grid.filterStrategy; + gridData = DataUtil.filter(gridData, filteringState, grid); } @@ -564,10 +575,9 @@ export abstract class IgxBaseExporter { const filteringState: IFilteringState = { expressionsTree: grid.filteringExpressionsTree, advancedExpressionsTree: grid.advancedFilteringExpressionsTree, + strategy: (grid.filterStrategy) ? grid.filterStrategy : new TreeGridFilteringStrategy() }; - filteringState.strategy = (grid.filterStrategy) ? grid.filterStrategy : new TreeGridFilteringStrategy(); - gridData = filteringState.strategy .filter(gridData, filteringState.expressionsTree, filteringState.advancedExpressionsTree); } @@ -675,9 +685,9 @@ export abstract class IgxBaseExporter { } } - private getColumns(columns: any): any { - const colList = new Array(columns.length); - const colWidthList = new Array(columns.filter(c => !c.hidden).length); + private getColumns(columns: IgxColumnComponent[]): IColumnList { + const colList = new Array(columns.length); + const colWidthList = new Array(columns.filter(c => !c.hidden).length); const hiddenColumns = []; let indexOfLastPinnedColumn = -1; let lastVisibleColumnIndex = -1; @@ -688,7 +698,7 @@ export abstract class IgxBaseExporter { const index = this.options.ignoreColumnsOrder || this.options.ignoreColumnsVisibility ? column.index : column.visibleIndex; const columnWidth = Number(column.width?.slice(0, -2)) || DEFAULT_COLUMN_WIDTH; - const columnInfo = { + const columnInfo: IColumnInfo = { header: columnHeader, dataType: column.dataType, field: column.field, @@ -715,9 +725,9 @@ export abstract class IgxBaseExporter { colList[++lastVisibleColumnIndex] = hiddenColumn; }); - const result = { - colList, - colWidthList, + const result: IColumnList = { + columns: colList, + columnWidths: colWidthList, indexOfLastPinnedColumn }; @@ -726,15 +736,9 @@ export abstract class IgxBaseExporter { private mapHierarchicalGridColumns(island: IgxRowIslandComponent) { const islandColumnList = island.childColumns.toArray(); - const colDefinitions = this.getColumns(islandColumnList); - - const mapRecord: IMapRecord = { - columns: colDefinitions.colList, - columnWidths: colDefinitions.colWidthList, - indexOfLastPinnedColumn: colDefinitions.indexOfLastPinnedColumn, - }; + const columnList = this.getColumns(islandColumnList); - this._ownersMap.set(island, mapRecord); + this._ownersMap.set(island, columnList); if (island.children.length > 0) { for (const childIsland of island.children) { diff --git a/projects/igniteui-angular/src/lib/services/exporter-common/export-utilities.ts b/projects/igniteui-angular/src/lib/services/exporter-common/export-utilities.ts index 1996b25e8d9..45fada1c1b9 100644 --- a/projects/igniteui-angular/src/lib/services/exporter-common/export-utilities.ts +++ b/projects/igniteui-angular/src/lib/services/exporter-common/export-utilities.ts @@ -19,7 +19,7 @@ export class ExportUtilities { const keys = new Set(keys1.concat(keys2).concat(keys3)); - return !ExportUtilities.isSpecialData(data) ? Array.from(keys) : [ 'Column 1' ]; + return !ExportUtilities.isSpecialData(dataEntry) ? Array.from(keys) : [ 'Column 1' ]; } public static saveBlobToFile(blob: Blob, fileName) { @@ -49,11 +49,10 @@ export class ExportUtilities { return buf; } - public static isSpecialData(data: any[]): boolean { - const dataEntry = data[0]; - return (typeof dataEntry === 'string' || - typeof dataEntry === 'number' || - dataEntry instanceof Date); + public static isSpecialData(data: any): boolean { + return (typeof data === 'string' || + typeof data === 'number' || + data instanceof Date); } public static hasValue(value: any): boolean { From 404743b453aa9cf3e272acb851f3aaf1ff97a110 Mon Sep 17 00:00:00 2001 From: IBarakov Date: Tue, 13 Apr 2021 12:08:04 +0300 Subject: [PATCH 06/15] chore(*): add type to owner property --- .../src/lib/services/exporter-common/base-export-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2c795957cdf..9c0e66b4acd 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 @@ -30,7 +30,7 @@ export interface IExportRecord { data: any; level: number; type: ExportRecordType; - owner?: any; + owner?: string | IgxGridBaseDirective; hidden?: boolean; } From 7b3bba5615b012ade65be9c5b6663960cf7fd0e1 Mon Sep 17 00:00:00 2001 From: IBarakov Date: Fri, 16 Apr 2021 00:40:19 +0300 Subject: [PATCH 07/15] feat(hierarchical-grid): add tests for hGrid export + address PR comments --- .../excel/excel-exporter-grid.spec.ts | 115 ++++++++- .../src/lib/services/excel/excel-exporter.ts | 5 +- .../src/lib/services/excel/excel-files.ts | 2 +- .../lib/services/excel/jszip-helper.spec.ts | 39 +-- .../excel/jszip-verification-wrapper.spec.ts | 21 +- .../services/excel/test-data.service.spec.ts | 102 +++++++- .../src/lib/services/excel/worksheet-data.ts | 2 +- .../exporter-common/base-export-service.ts | 8 +- .../hierarchical-grid-components.spec.ts | 41 +++ .../lib/test-utils/sample-test-data.spec.ts | 235 ++++++++++++++++++ 10 files changed, 520 insertions(+), 50 deletions(-) diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts index c0dc7adbc78..9a149b988ef 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts @@ -1,4 +1,4 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed, tick, waitForAsync } from '@angular/core/testing'; import { Component, ViewChild } from '@angular/core'; import { IgxGridModule } from '../../grids/grid/public_api'; import { IgxGridComponent } from '../../grids/grid/grid.component'; @@ -26,10 +26,16 @@ import { configureTestSuite } from '../../test-utils/configure-suite'; import { IgxTreeGridPrimaryForeignKeyComponent } from '../../test-utils/tree-grid-components.spec'; import { IgxTreeGridModule, IgxTreeGridComponent } from '../../grids/tree-grid/public_api'; import { IgxNumberFilteringOperand } from '../../data-operations/filtering-condition'; -import { wait } from '../../test-utils/ui-interactions.spec'; +import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { FilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree'; import { FilteringLogic } from '../../data-operations/filtering-expression.interface'; +import { IgxHierarchicalGridExportComponent } from '../../test-utils/hierarchical-grid-components.spec'; +import { IgxHierarchicalGridModule, + IgxHierarchicalGridComponent, + IgxHierarchicalRowComponent +} from '../../grids/hierarchical-grid/public_api'; +import { GridFunctions } from '../../test-utils/grid-functions.spec'; describe('Excel Exporter', () => { configureTestSuite(); @@ -47,9 +53,10 @@ describe('Excel Exporter', () => { GridWithEmptyColumnsComponent, GridIDNameJobTitleHireDataPerformanceComponent, GridHireDateComponent, - GridExportGroupedDataComponent + GridExportGroupedDataComponent, + IgxHierarchicalGridExportComponent ], - imports: [IgxGridModule, IgxTreeGridModule, NoopAnimationsModule] + imports: [IgxGridModule, IgxTreeGridModule, IgxHierarchicalGridModule, NoopAnimationsModule] }).compileComponents(); })); @@ -632,6 +639,101 @@ describe('Excel Exporter', () => { }); }); + describe('', () => { + let fix; + let hGrid: IgxHierarchicalGridComponent; + + beforeEach(waitForAsync(() => { + options = createExportOptions('HierarchicalGridExcelExport'); + fix = TestBed.createComponent(IgxHierarchicalGridExportComponent); + fix.detectChanges(); + + hGrid = fix.componentInstance.hGrid; + })); + + it('should export hierarchical grid', async () => { + await exportAndVerify(hGrid, options, actualData.exportHierarchicalData); + }); + + it('should export hierarchical grid respecting options width.', async () => { + options = createExportOptions('HierarchicalGridExcelExport', 50); + await exportAndVerify(hGrid, options, actualData.exportHierarchicalDataWithColumnWidth); + }); + + it('should export sorted hierarchical grid data', async () => { + hGrid.sort({fieldName: 'GrammyNominations', dir: SortingDirection.Desc}); + + fix.detectChanges(); + + await exportAndVerify(hGrid, options, actualData.exportSortedHierarchicalData); + }); + + it('should export hierarchical grid data with ignored sorting', async () => { + hGrid.sort({fieldName: 'GrammyNominations', dir: SortingDirection.Desc}); + + options.ignoreSorting = true; + fix.detectChanges(); + + await exportAndVerify(hGrid, options, actualData.exportHierarchicalData); + }); + + it('should export filtered hierarchical grid data', async () => { + hGrid.filter('Debut', '2009', IgxStringFilteringOperand.instance().condition('contains'), true); + fix.detectChanges(); + + await exportAndVerify(hGrid, options, actualData.exportFilteredHierarchicalData); + }); + + it('should export hierarchical grid data with ignored filtering', async () => { + hGrid.filter('Debut', '2009', IgxStringFilteringOperand.instance().condition('contains'), true); + fix.detectChanges(); + + options.ignoreFiltering = true; + + await exportAndVerify(hGrid, options, actualData.exportHierarchicalData); + }); + + it('should export hierarchical grid with expanded rows.', async () => { + const firstRow = hGrid.getRowByIndex(0) as IgxHierarchicalRowComponent; + const secondRow = hGrid.getRowByIndex(1) as IgxHierarchicalRowComponent; + + UIInteractions.simulateClickAndSelectEvent(firstRow.expander); + fix.detectChanges(); + expect(firstRow.expanded).toBe(true); + + let childGrids = hGrid.hgridAPI.getChildGrids(false); + + const firstChildGrid = childGrids[0]; + const firstChildRow = firstChildGrid.getRowByIndex(2) as IgxHierarchicalRowComponent; + + UIInteractions.simulateClickAndSelectEvent(firstChildRow.expander); + fix.detectChanges(); + expect(firstChildRow.expanded).toBe(true); + + const secondChildGrid = childGrids[1]; + const secondChildRow = secondChildGrid.getRowByIndex(0) as IgxHierarchicalRowComponent; + + UIInteractions.simulateClickAndSelectEvent(secondChildRow.expander); + fix.detectChanges(); + expect(secondChildRow.expanded).toBe(true); + + UIInteractions.simulateClickAndSelectEvent(secondRow.expander); + fix.detectChanges(); + expect(secondRow.expanded).toBe(true); + + childGrids = hGrid.hgridAPI.getChildGrids(false); + + const thirdChildGrid = childGrids[3]; + const thirdChildRow = thirdChildGrid.getRowByIndex(0) as IgxHierarchicalRowComponent; + + UIInteractions.simulateClickAndSelectEvent(thirdChildRow.expander); + fix.detectChanges(); + expect(thirdChildRow.expanded).toBe(true); + + await exportAndVerify(hGrid, options, actualData.exportHierarchicalDataWithExpandedRows); + }); + }); + describe('', () => { let fix; let treeGrid: IgxTreeGridComponent; @@ -855,9 +957,10 @@ describe('Excel Exporter', () => { }; const exportAndVerify = async (component, exportOptions, expectedData) => { + const isHGrid = component instanceof IgxHierarchicalGridComponent; const wrapper = await getExportedData(component, exportOptions); - await wrapper.verifyStructure(); - await wrapper.verifyDataFilesContent(expectedData); + await wrapper.verifyStructure(isHGrid); + await wrapper.verifyDataFilesContent(expectedData, '', isHGrid); }; }); diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts index 0699f642807..7dcce2802cc 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts @@ -97,10 +97,11 @@ export class IgxExcelExporterService extends IgxBaseExporter { rootKeys = this._ownersMap.get(firstDataElement.owner).columns.map(c => c.field); } else { const defaultOwner = this._ownersMap.get(DEFAULT_OWNER); + const columns = defaultOwner.columns.filter(col => !col.skip); columnWidths = defaultOwner.columnWidths; indexOfLastPinnedColumn = defaultOwner.indexOfLastPinnedColumn; - columnCount = defaultOwner.columns.length; - rootKeys = defaultOwner.columns.map(c => c.field); + columnCount = columns.length; + rootKeys = columns.map(c => !ExportUtilities.isNullOrWhitespaces(c.field) ? c.field : c.header); } } diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts index 0f2bf67820d..4ea69415c1e 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-files.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-files.ts @@ -172,7 +172,7 @@ export class WorksheetFile implements IExcelFile { rowData[0] = ``; - const keys = Object.keys(record.data); + const keys = worksheetData.isSpecialData ? [record.data] : Object.keys(record.data); for (let j = 0; j < keys.length; j++) { const col = j + (isHierarchicalGrid ? rowLevel : 0); diff --git a/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts b/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts index b471429bb49..ce899f1600c 100644 --- a/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts @@ -33,6 +33,11 @@ export class JSZipFiles { 'xl/sharedStrings.xml' ]; + public static hGridDataFilesAndFoldersNames = [ + 'xl/worksheets/sheet1.xml', + 'xl/sharedStrings.xml' + ]; + public static templatesNames = [ '_rels/', '_rels/.rels', @@ -163,29 +168,29 @@ export class JSZipFiles { `; } - public static getSheetDataFile(sheetData: string, hasValues) { + public static getSheetDataFile(sheetData: string, hasValues: boolean, isHGrid: boolean) { if (hasValues) { - return ` -${ sheetData }` + -`` + -``; + const tablePart = isHGrid ? '' : ''; + return ` + ${ sheetData }` + + `${tablePart}`; } else { return ` -` + -``; + ` + + ``; } } - public static createExpectedXML(xmlFile: ExcelFileTypes, currentData = '', hasValues = true): any { + public static createExpectedXML(xmlFile: ExcelFileTypes, currentData = '', hasValues = true, isHGrid: boolean = false): any { let resultXml; switch (xmlFile) { case ExcelFileTypes.RootRelsFile: @@ -276,7 +281,7 @@ export class JSZipFiles { case ExcelFileTypes.WorksheetFile: resultXml = { name: JSZipFiles.templatesNames[11], - content : JSZipFiles.getSheetDataFile(currentData, hasValues) + content : JSZipFiles.getSheetDataFile(currentData, hasValues, isHGrid) }; break; case ExcelFileTypes.ContentTypesFile: diff --git a/projects/igniteui-angular/src/lib/services/excel/jszip-verification-wrapper.spec.ts b/projects/igniteui-angular/src/lib/services/excel/jszip-verification-wrapper.spec.ts index e1c52fb2146..f2df05530be 100644 --- a/projects/igniteui-angular/src/lib/services/excel/jszip-verification-wrapper.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/jszip-verification-wrapper.spec.ts @@ -19,11 +19,12 @@ export class JSZipWrapper { } /* Asserts the JSZip contains the files it should contain. */ - public verifyStructure(message = '') { + public verifyStructure(isHGrid: boolean = false, message = '') { let result = ObjectComparer.AreEqual(this.templateFilesAndFolders, JSZipFiles.templatesNames); + const template = isHGrid ? JSZipFiles.hGridDataFilesAndFoldersNames : JSZipFiles.dataFilesAndFoldersNames; result = (this.hasValues) ? - result && ObjectComparer.AreEqual(this.dataFilesAndFolders, JSZipFiles.dataFilesAndFoldersNames) : + result && ObjectComparer.AreEqual(this.dataFilesAndFolders, template) : result && this._filesAndFolders.length === JSZipFiles.templatesNames.length; expect(result).toBe(true, message + ' Unexpected zip structure!'); @@ -45,12 +46,12 @@ export class JSZipWrapper { /* Verifies the contents of all data files and asserts the result. Optionally, a message can be passed in, which, if specified, will be shown in the beginning of the comparison result. */ - public async verifyDataFilesContent(expectedData: IFileContent[], message = '') { + public async verifyDataFilesContent(expectedData: IFileContent[], message = '', isHGrid = false) { let result; const msg = (message !== '') ? message + '\r\n' : ''; await this.readDataFiles().then(() => { - result = this.compareFiles(this.dataFilesContent, expectedData); + result = this.compareFiles(this.dataFilesContent, expectedData, isHGrid); expect(result.areEqual).toBe(true, msg + result.differences); }); } @@ -142,10 +143,10 @@ export class JSZipWrapper { } /* Compares the content of two files based on the provided file type and expected value data. */ - private compareFilesContent(currentContent: string, fileType: ExcelFileTypes, fileData: string) { + private compareFilesContent(currentContent: string, fileType: ExcelFileTypes, fileData: string, isHGrid) { let result = true; let differences = ''; - const expectedFile = JSZipFiles.createExpectedXML(fileType, fileData, this.hasValues); + const expectedFile = JSZipFiles.createExpectedXML(fileType, fileData, this.hasValues, isHGrid); const expectedContent = expectedFile.content; result = ObjectComparer.AreEqualXmls(currentContent, expectedContent); if (!result) { @@ -155,14 +156,14 @@ export class JSZipWrapper { return { areEqual: result, differences }; } - private compareContent(currentFile: IFileContent, expectedData: string) { + private compareContent(currentFile: IFileContent, expectedData: string, isHGrid) { let result = true; let differences = ''; const fileType = this.getFileTypeByName(currentFile.fileName); if (fileType !== undefined) { - const comparisonResult = this.compareFilesContent(currentFile.fileContent, fileType, expectedData); + const comparisonResult = this.compareFilesContent(currentFile.fileContent, fileType, expectedData, isHGrid); result = comparisonResult.areEqual; if (!result) { differences = comparisonResult.differences; @@ -174,13 +175,13 @@ export class JSZipWrapper { } /* Compares the contents of the provided files to their expected values. */ - private compareFiles(actualFilesContent: IFileContent[], expectedFilesData: IFileContent[]) { + private compareFiles(actualFilesContent: IFileContent[], expectedFilesData: IFileContent[], isHGrid: boolean = false) { let result = true; let differences = ''; for (const current of actualFilesContent) { const index = (expectedFilesData !== undefined) ? expectedFilesData.findIndex((f) => f.fileName === current.fileName) : -1; const excelData = (index > -1 && expectedFilesData[index] !== undefined) ? expectedFilesData[index].fileContent : ''; - const comparisonResult = this.compareContent(current, excelData); + const comparisonResult = this.compareContent(current, excelData, isHGrid); result = result && comparisonResult.areEqual; if (!comparisonResult.areEqual) { differences = differences + comparisonResult.differences; diff --git a/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts b/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts index 723ce8b327a..a9308717852 100644 --- a/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts @@ -240,15 +240,20 @@ export class FileContentData { constructor() {} - public create(worksheetData: string, tableData: string, sharedStringsData: string, workbookData: string, appData: string): IFileContent[] { + public create(worksheetData: string, tableData: string, sharedStringsData: string, workbookData: string, appData: string, isHGrid: boolean = false): IFileContent[] { this._fileContentCollection = [ { fileName: JSZipFiles.dataFiles[1].name, fileContent : worksheetData }, - { fileName: JSZipFiles.dataFiles[2].name, fileContent : tableData }, { fileName: JSZipFiles.dataFiles[3].name, fileContent : sharedStringsData }, { fileName: JSZipFiles.templateFiles[6].name, fileContent : workbookData }, { fileName: JSZipFiles.templateFiles[1].name, fileContent : appData }, ]; + if (!isHGrid) { + this._fileContentCollection.push({ + fileName: JSZipFiles.dataFiles[2].name, fileContent : tableData + }); + } + return this._fileContentCollection; } @@ -351,8 +356,8 @@ export class FileContentData { return this.createData(); } - private createData() { - return this.create(this._worksheetData, this._tableData, this._sharedStringsData, this._workbookData, this._appData); + private createData(isHGrid: boolean = false) { + return this.create(this._worksheetData, this._tableData, this._sharedStringsData, this._workbookData, this._appData, isHGrid); } public get differentTypesDataContent() { @@ -1064,7 +1069,7 @@ export class FileContentData { public get exportGroupedDataWithIgnoreSorting() { this._sharedStringsData = - `count="30" uniqueCount="21">PriceModelEditionBrand: Tesla (3)75000Model SSport100000RoadsterPerformance65000BaseBrand: BMW (2)150000M5CompetitionBrand: VW (3)ArteonBusinessPassatR Line`; + `count="22" uniqueCount="17">PriceModelEditionBrand: Tesla (3)Model SSportRoadsterPerformanceBaseBrand: BMW (2)M5CompetitionBrand: VW (3)ArteonBusinessPassatR Line`; this._tableData = `ref="A1:C12" totalsRowShown="0"> @@ -1076,14 +1081,14 @@ export class FileContentData { - 012345678910511121314157149164171810191871720`; + 0123750004510000067650004891500001011100000107127500013146500015141000001316`; return this.createData(); } public get exportGroupedDataWithIgnoreFiltering() { this._sharedStringsData = - `count="30" uniqueCount="21">PriceModelEditionBrand: BMW (2)150000M5Competition100000PerformanceBrand: Tesla (3)75000Model SSportRoadster65000BaseBrand: VW (3)ArteonBusinessPassatR Line`; + `count="22" uniqueCount="17">PriceModelEditionBrand: BMW (2)M5CompetitionPerformanceBrand: Tesla (3)Model SSportRoadsterBaseBrand: VW (3)ArteonBusinessPassatR Line`; this._tableData = `ref="A1:C12" totalsRowShown="0"> @@ -1095,7 +1100,7 @@ export class FileContentData { - 0123456758910111271381411151610171814191871720`; + 012315000045100000467750008910000010665000811127500013146500015141000001316`; return this.createData(); } @@ -1118,4 +1123,85 @@ export class FileContentData { return this.createData(); } + public get exportHierarchicalData() { + this._sharedStringsData = + `count="106" uniqueCount="57">ArtistDebutGrammyNominationsGrammyAwardsNaomí YepesAlbumLaunch DateBillboard ReviewUS Billboard 200Pushing up daisiesNo.TitleReleasedGenreWood Shavifdsafdsafsangs Forever*fdasfsaWood Shavifdsafdsafsavngs Forever*vxzvczxWfdsafsaings Forever*fdsacewwwqwqWood Shavings Forever*rewqrqcxzPushing up daisies - DeluxeWood Shavings Forever - RemixPunkUtopiaSANTORINIHip-HopHEARTBEATOVERSEASWish You Were HereZoomDo You?No PhotosTourStarted onLocationHeadlinerFaithful TourSep 12WorldwideNOCountryTickets SoldAttendantsBelgiumUSABabila EbwéléFahrenheitShow OutMood SwingsScenarioAstroworldJul 21BulgariaRomaniaChloe`; + + this._worksheetData = + ` + + + + 0123420116047200901156201531`; + + return this.createData(); + } + + public get exportHierarchicalDataWithColumnWidth() { + this._sharedStringsData = + `count="106" uniqueCount="57">ArtistDebutGrammyNominationsGrammyAwardsNaomí YepesAlbumLaunch DateBillboard ReviewUS Billboard 200Pushing up daisiesNo.TitleReleasedGenreWood Shavifdsafdsafsangs Forever*fdasfsaWood Shavifdsafdsafsavngs Forever*vxzvczxWfdsafsaings Forever*fdsacewwwqwqWood Shavings Forever*rewqrqcxzPushing up daisies - DeluxeWood Shavings Forever - RemixPunkUtopiaSANTORINIHip-HopHEARTBEATOVERSEASWish You Were HereZoomDo You?No PhotosTourStarted onLocationHeadlinerFaithful TourSep 12WorldwideNOCountryTickets SoldAttendantsBelgiumUSABabila EbwéléFahrenheitShow OutMood SwingsScenarioAstroworldJul 21BulgariaRomaniaChloe`; + + this._worksheetData = + ` + + + + 0123420116047200901156201531`; + + return this.createData(); + } + + public get exportHierarchicalDataWithExpandedRows() { + this._sharedStringsData = + `count="106" uniqueCount="57">ArtistDebutGrammyNominationsGrammyAwardsNaomí YepesAlbumLaunch DateBillboard ReviewUS Billboard 200Pushing up daisiesNo.TitleReleasedGenreWood Shavifdsafdsafsangs Forever*fdasfsaWood Shavifdsafdsafsavngs Forever*vxzvczxWfdsafsaings Forever*fdsacewwwqwqWood Shavings Forever*rewqrqcxzPushing up daisies - DeluxeWood Shavings Forever - RemixPunkUtopiaSANTORINIHip-HopHEARTBEATOVERSEASWish You Were HereZoomDo You?No PhotosTourStarted onLocationHeadlinerFaithful TourSep 12WorldwideNOCountryTickets SoldAttendantsBelgiumUSABabila EbwéléFahrenheitShow OutMood SwingsScenarioAstroworldJul 21BulgariaRomaniaChloe`; + + this._worksheetData = + ` + + + + 01234201160567892000-05-31T00:00:008642222001-05-31T00:00:00122252021-12-19T00:00:0011101112131262021-12-19T00:00:00272282021-12-19T00:00:00273292021-12-19T00:00:0027302020-07-17T00:00:00533435363738394041424344451000010000461923001865233839404138394041383940414720090115678482000-05-31T00:00:008642343536375253404142434454250001982255650216332056201531`; + + return this.createData(); + } + + public get exportSortedHierarchicalData() { + this._sharedStringsData = + `count="106" uniqueCount="57">ArtistDebutGrammyNominationsGrammyAwardsNaomí YepesAlbumLaunch DateBillboard ReviewUS Billboard 200Pushing up daisiesNo.TitleReleasedGenreWood Shavifdsafdsafsangs Forever*fdasfsaWood Shavifdsafdsafsavngs Forever*vxzvczxWfdsafsaings Forever*fdsacewwwqwqWood Shavings Forever*rewqrqcxzPushing up daisies - DeluxeWood Shavings Forever - RemixPunkUtopiaSANTORINIHip-HopHEARTBEATOVERSEASWish You Were HereZoomDo You?No PhotosTourStarted onLocationHeadlinerFaithful TourSep 12WorldwideNOCountryTickets SoldAttendantsBelgiumUSAChloeBabila EbwéléFahrenheitShow OutMood SwingsScenarioAstroworldJul 21BulgariaRomania`; + + this._worksheetData = + ` + + + + 0123420116047201531482009011`; + + return this.createData(); + } + + public get exportFilteredHierarchicalData() { + this._sharedStringsData = + `count="33" uniqueCount="31">ArtistDebutGrammyNominationsGrammyAwardsBabila EbwéléAlbumLaunch DateBillboard ReviewUS Billboard 200FahrenheitNo.TitleReleasedGenreShow OutHip-HopMood SwingsScenarioTourStarted onLocationHeadlinerAstroworldJul 21WorldwideNOCountryTickets SoldAttendantsBulgariaRomania`; + + this._worksheetData = + ` + + + + 012342009011`; + + return this.createData(); + } + + + + + + + + + + + + } diff --git a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts index 3854db790fa..c80e39bf1b4 100644 --- a/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts +++ b/projects/igniteui-angular/src/lib/services/excel/worksheet-data.ts @@ -48,7 +48,7 @@ export class WorksheetData { this.options.exportAsTable = false; } - this._isSpecialData = ExportUtilities.isSpecialData(this._data[0]); + this._isSpecialData = ExportUtilities.isSpecialData(this._data[0].data); this._rowCount = this._data.length + 1; this._dataDictionary = new WorksheetDataDictionary(this.columnCount, this.options.columnWidth, this.columnWidths); } 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 9c0e66b4acd..fac5f82d482 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 @@ -156,7 +156,7 @@ export abstract class IgxBaseExporter { if (options === undefined || options === null) { throw Error('No options provided!'); } - + //options.ignoreSorting = true; this.options = options; const columns = grid.columnList.toArray(); @@ -630,7 +630,7 @@ export abstract class IgxBaseExporter { return; } - const firstCol = this._ownersMap.get(grid).columns[0].field; + const firstCol = this._ownersMap.get(DEFAULT_OWNER).columns[0].field; for (const record of records) { let recordVal = record.value; @@ -660,7 +660,6 @@ export abstract class IgxBaseExporter { level: record.level, hidden: !parentExpanded, type: ExportRecordType.GroupedRecord, - owner: grid }; this.flatRecords.push(groupExpression); @@ -676,7 +675,6 @@ export abstract class IgxBaseExporter { level: record.level + 1, hidden: !(expanded && parentExpanded), type: ExportRecordType.DataRecord, - owner: grid }; this.flatRecords.push(currentRecord); @@ -686,7 +684,7 @@ export abstract class IgxBaseExporter { } private getColumns(columns: IgxColumnComponent[]): IColumnList { - const colList = new Array(columns.length); + const colList = []; const colWidthList = new Array(columns.filter(c => !c.hidden).length); const hiddenColumns = []; let indexOfLastPinnedColumn = -1; diff --git a/projects/igniteui-angular/src/lib/test-utils/hierarchical-grid-components.spec.ts b/projects/igniteui-angular/src/lib/test-utils/hierarchical-grid-components.spec.ts index 4b95e589943..35637659da2 100644 --- a/projects/igniteui-angular/src/lib/test-utils/hierarchical-grid-components.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/hierarchical-grid-components.spec.ts @@ -305,3 +305,44 @@ export class IgxHierGridExternalAdvancedFilteringComponent extends IgxHierarchic public data = SampleTestData.generateHGridData(5, 3); } + +@Component({ + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` +}) +export class IgxHierarchicalGridExportComponent { + @ViewChild('hierarchicalGrid', { read: IgxHierarchicalGridComponent, static: true }) public hGrid: IgxHierarchicalGridComponent; + public data = SampleTestData.hierarchicalGridExportData(); +} diff --git a/projects/igniteui-angular/src/lib/test-utils/sample-test-data.spec.ts b/projects/igniteui-angular/src/lib/test-utils/sample-test-data.spec.ts index 52fa593645c..7d84b77df3d 100644 --- a/projects/igniteui-angular/src/lib/test-utils/sample-test-data.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/sample-test-data.spec.ts @@ -1601,6 +1601,241 @@ export class SampleTestData { { Price: 100000, Brand: 'VW', Model: 'Arteon', Edition: 'R Line' }, ]); + /* Data fields: Artist, Debut, GrammyNominations, GrammyAwards, Tours, Albums, Songs */ + public static hierarchicalGridExportData = () => ([ + { + Artist: 'Naomí Yepes', + Debut: 2011, + GrammyNominations: 6, + GrammyAwards: 0, + HasGrammyAward: false, + Tours: [ + { + Tour: 'Faithful Tour', + StartedOn: 'Sep 12', + Location: 'Worldwide', + Headliner: 'NO', + TouredBy: 'Naomí Yepes', + TourData: [ + { + Country: 'Belgium', + TicketsSold: 10000, + Attendants: 10000, + }, + { + Country: 'USA', + TicketsSold: 192300, + Attendants: 186523, + } + ] + }, + { + Tour: 'Faithful Tour', + StartedOn: 'Sep 12', + Location: 'Worldwide', + Headliner: 'NO', + TouredBy: 'Naomí Yepes' + }, + { + Tour: 'Faithful Tour', + StartedOn: 'Sep 12', + Location: 'Worldwide', + Headliner: 'NO', + TouredBy: 'Naomí Yepes' + }, + { + Tour: 'Faithful Tour', + StartedOn: 'Sep 12', + Location: 'Worldwide', + Headliner: 'NO', + TouredBy: 'Naomí Yepes' + } + ], + Albums: [ + { + Album: 'Pushing up daisies', + LaunchDate: new Date('May 31, 2000'), + BillboardReview: 86, + USBillboard200: 42, + Artist: 'Naomí Yepes', + Songs: [ + { + Number: 1, + Title: 'Wood Shavifdsafdsafsangs Forever', + Released: new Date('9 Jun 2019'), + Genre: '*fdasfsa', + Album: 'Pushing up daisies' + }, + { + Number: 2, + Title: 'Wood Shavifdsafdsafsavngs Forever', + Released: new Date('9 Jun 2019'), + Genre: '*vxzvczx', + Album: 'Pushing up daisies' + }, + { + Number: 3, + Title: 'Wfdsafsaings Forever', + Released: new Date('9 Jun 2019'), + Genre: '*fdsacewwwqwq', + Album: 'Pushing up daisies' + }, + { + Number: 4, + Title: 'Wood Shavings Forever', + Released: new Date('9 Jun 2019'), + Genre: '*rewqrqcxz', + Album: 'Pushing up daisies' + }, + ] + }, + { + Album: 'Pushing up daisies - Deluxe', + LaunchDate: new Date('May 31, 2001'), + BillboardReview: 12, + USBillboard200: 2, + Artist: 'Naomí Yepes', + Songs: [ + { + Number: 1, + Title: 'Wood Shavings Forever - Remix', + Released: new Date('9 Jun 2020'), + Genre: 'Punk', + Album: 'Pushing up daisies' + }, + ] + }, + { + Album: 'Utopia', + LaunchDate: new Date('Dec 19, 2021'), + BillboardReview: 1, + USBillboard200: 1, + Artist: 'Naomí Yepes', + Songs: [ + { + Number: 1, + Title: 'SANTORINI', + Released: new Date('19 Dec 2021'), + Genre: 'Hip-Hop', + Album: 'Utopia' + }, + { + Number: 2, + Title: 'HEARTBEAT', + Released: new Date('19 Dec 2021'), + Genre: 'Hip-Hop', + Album: 'Utopia' + }, + { + Number: 3, + Title: 'OVERSEAS', + Released: new Date('19 Dec 2021'), + Genre: 'Hip-Hop', + Album: 'Utopia' + }, + ] + }, + { + Album: 'Wish You Were Here', + LaunchDate: new Date('Jul 17, 2020'), + BillboardReview: 5, + USBillboard200: 3, + Artist: 'Naomí Yepes', + Songs: [ + { + Number: 1, + Title: 'Zoom', + Released: new Date('17 Jul 2020'), + Genre: 'Hip-Hop', + Album: 'Wish You Were Here' + }, + { + Number: 2, + Title: 'Do You?', + Released: new Date('17 Jul 2020'), + Genre: 'Hip-Hop', + Album: 'Wish You Were Here' + }, + { + Number: 3, + Title: 'No Photos', + Released: new Date('17 Jul 2020'), + Genre: 'Hip-Hop', + Album: 'Wish You Were Here' + }, + ] + } + ] + }, + { + Artist: 'Babila Ebwélé', + Debut: 2009, + GrammyNominations: 0, + GrammyAwards: 11, + HasGrammyAward: true, + Albums: [ + { + Album: 'Fahrenheit', + LaunchDate: new Date('May 31, 2000'), + BillboardReview: 86, + USBillboard200: 42, + Artist: 'Babila Ebwélé', + Songs: [ + { + Number: 1, + Title: 'Show Out', + Released: new Date('17 Jul 2020'), + Genre: 'Hip-Hop', + Album: 'Fahrenheit' + }, + { + Number: 2, + Title: 'Mood Swings', + Released: new Date('17 Jul 2020'), + Genre: 'Hip-Hop', + Album: 'Fahrenheit' + }, + { + Number: 3, + Title: 'Scenario', + Released: new Date('17 Jul 2020'), + Genre: 'Hip-Hop', + Album: 'Fahrenheit' + }, + ] + } + ], + Tours: [ + { + Tour: 'Astroworld', + StartedOn: 'Jul 21', + Location: 'Worldwide', + Headliner: 'NO', + TouredBy: 'Babila Ebwélé', + TourData: [ + { + Country: 'Bulgaria', + TicketsSold: 25000, + Attendants: 19822, + }, + { + Country: 'Romania', + TicketsSold: 65021, + Attendants: 63320, + } + ] + }, + ] + }, + { + Artist: 'Chloe', + Debut: 2015, + GrammyNominations: 3, + GrammyAwards: 1, + HasGrammyAward: true, + } + ]); + /** * Generates simple array of primitve values * From c72e9be21ddc7cceebdef221a5714a9fab6207fd Mon Sep 17 00:00:00 2001 From: IBarakov Date: Fri, 16 Apr 2021 12:53:24 +0300 Subject: [PATCH 08/15] chore(*): minor changes regarding PR comments --- .../src/lib/services/excel/excel-strings.ts | 31 ++----------------- .../services/excel/test-data.service.spec.ts | 12 ------- .../exporter-common/base-export-service.ts | 5 +-- 3 files changed, 5 insertions(+), 43 deletions(-) diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts b/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts index f6db23215c8..d364a6d893b 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-strings.ts @@ -21,35 +21,8 @@ export class ExcelStrings { } public static getStyles(hasNumberValues: boolean, hasDateValues: boolean, isHierarchicalGrid: boolean): string { - let cellXFCount = hasDateValues ? 3 : hasNumberValues ? 2 : 1; - const fontsCount = isHierarchicalGrid ? 2 : 1; - const fillsCount = isHierarchicalGrid ? 3 : 2; - - const additionalFill = fillsCount === 3 ? - '' : - ''; - - const additionalFont = fontsCount === 2 ? - '' : - ''; - - - const fills = `${additionalFill}`; - const fonts = `${additionalFont}`; - - let additionalCellXF = ''; - - if (hasDateValues) { - additionalCellXF = additionalCellXF + ' '; - } - - if(isHierarchicalGrid) { - cellXFCount++; - additionalCellXF = additionalCellXF + ' '; - } - - - return ExcelStrings.XML_STRING + '' + fonts + fills + '' + additionalCellXF + ''; + return ExcelStrings.XML_STRING + + ' '; } public static getWorkbook(worksheetName: string): string { diff --git a/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts b/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts index a9308717852..cbd3bc8f2a6 100644 --- a/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/test-data.service.spec.ts @@ -1192,16 +1192,4 @@ export class FileContentData { return this.createData(); } - - - - - - - - - - - - } 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 fac5f82d482..a3263608fff 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 @@ -685,12 +685,12 @@ export abstract class IgxBaseExporter { private getColumns(columns: IgxColumnComponent[]): IColumnList { const colList = []; - const colWidthList = new Array(columns.filter(c => !c.hidden).length); + const colWidthList = []; const hiddenColumns = []; let indexOfLastPinnedColumn = -1; let lastVisibleColumnIndex = -1; - columns.forEach((column, i) => { + columns.forEach((column) => { const columnHeader = !ExportUtilities.isNullOrWhitespaces(column.header) ? column.header : column.field; const exportColumn = !column.hidden || this.options.ignoreColumnsVisibility; const index = this.options.ignoreColumnsOrder || this.options.ignoreColumnsVisibility ? column.index : column.visibleIndex; @@ -748,6 +748,7 @@ export abstract class IgxBaseExporter { private resetDefaults() { this._sort = null; this.flatRecords = []; + this.options = {} as IgxExporterOptionsBase; this._ownersMap.clear(); } From 2e75f408bc18a4b429052c94a47989ac527d80ba Mon Sep 17 00:00:00 2001 From: IBarakov Date: Fri, 16 Apr 2021 14:08:22 +0300 Subject: [PATCH 09/15] chore(*): fix expected data for styles.xml --- .../src/lib/services/excel/jszip-helper.spec.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts b/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts index ce899f1600c..50b4256cd05 100644 --- a/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts @@ -129,20 +129,7 @@ export class JSZipFiles { const cellXfCount = this.hasDates ? 3 : 1; const additionalCellXf = this.hasDates ? ` ` : ''; - return ` - ` + - `` + - `` + - `` + `${additionalCellXf}` + - `` + - `` + - ``; + return ' '; } public static getSharedStringsXML(stringsData: string) { From 8ada2ee4d77c45a524c8158c6772beae9ea50535 Mon Sep 17 00:00:00 2001 From: gedinakova Date: Fri, 16 Apr 2021 18:21:25 +0300 Subject: [PATCH 10/15] test(Excel): Fixed styles.xml template --- .../src/lib/services/excel/jszip-helper.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts b/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts index 50b4256cd05..e91db27d953 100644 --- a/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/jszip-helper.spec.ts @@ -129,7 +129,7 @@ export class JSZipFiles { const cellXfCount = this.hasDates ? 3 : 1; const additionalCellXf = this.hasDates ? ` ` : ''; - return ' '; + return ' '; } public static getSharedStringsXML(stringsData: string) { From e903ac1bba17fd88f88ae4cdbee8efb2cdcd91ff Mon Sep 17 00:00:00 2001 From: gedinakova Date: Fri, 16 Apr 2021 18:40:31 +0300 Subject: [PATCH 11/15] test(Excel): Fixed failing col visibility tests. --- .../src/lib/services/excel/excel-exporter-grid.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts index d2db9abf431..0476e393c04 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts @@ -1,4 +1,4 @@ -import { TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { TestBed, waitForAsync } from '@angular/core/testing'; import { Component, ViewChild } from '@angular/core'; import { IgxGridModule } from '../../grids/grid/public_api'; import { IgxGridComponent } from '../../grids/grid/grid.component'; @@ -35,7 +35,6 @@ import { IgxHierarchicalGridModule, IgxHierarchicalGridComponent, IgxHierarchicalRowComponent } from '../../grids/hierarchical-grid/public_api'; -import { GridFunctions } from '../../test-utils/grid-functions.spec'; describe('Excel Exporter', () => { configureTestSuite(); @@ -147,7 +146,6 @@ describe('Excel Exporter', () => { await wait(); const grid = fix.componentInstance.grid; - options.ignoreColumnsOrder = true; options.ignoreColumnsVisibility = false; expect(grid.visibleColumns.length).toEqual(3, 'Invalid number of visible columns!'); From 04e6b4506f98439857db7781ea94c7d33a0b8fa9 Mon Sep 17 00:00:00 2001 From: IBarakov Date: Mon, 19 Apr 2021 11:34:01 +0300 Subject: [PATCH 12/15] chore(*): fix tests + address PR comments --- .../excel/excel-exporter-grid.spec.ts | 12 ++--- .../src/lib/services/excel/excel-exporter.ts | 4 +- .../exporter-common/base-export-service.ts | 47 ++++++++++++++++--- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts index 0476e393c04..72133037ce2 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts @@ -33,8 +33,8 @@ import { FilteringLogic } from '../../data-operations/filtering-expression.inter import { IgxHierarchicalGridExportComponent } from '../../test-utils/hierarchical-grid-components.spec'; import { IgxHierarchicalGridModule, IgxHierarchicalGridComponent, - IgxHierarchicalRowComponent } from '../../grids/hierarchical-grid/public_api'; +import { IgxHierarchicalRowComponent } from '../../grids/hierarchical-grid/hierarchical-row.component'; describe('Excel Exporter', () => { configureTestSuite(); @@ -692,8 +692,8 @@ describe('Excel Exporter', () => { }); it('should export hierarchical grid with expanded rows.', async () => { - const firstRow = hGrid.getRowByIndex(0) as IgxHierarchicalRowComponent; - const secondRow = hGrid.getRowByIndex(1) as IgxHierarchicalRowComponent; + const firstRow = hGrid.hgridAPI.get_row_by_index(0) as IgxHierarchicalRowComponent; + const secondRow = hGrid.hgridAPI.get_row_by_index(1) as IgxHierarchicalRowComponent; UIInteractions.simulateClickAndSelectEvent(firstRow.expander); fix.detectChanges(); @@ -702,14 +702,14 @@ describe('Excel Exporter', () => { let childGrids = hGrid.hgridAPI.getChildGrids(false); const firstChildGrid = childGrids[0]; - const firstChildRow = firstChildGrid.getRowByIndex(2) as IgxHierarchicalRowComponent; + const firstChildRow = firstChildGrid.hgridAPI.get_row_by_index(2) as IgxHierarchicalRowComponent; UIInteractions.simulateClickAndSelectEvent(firstChildRow.expander); fix.detectChanges(); expect(firstChildRow.expanded).toBe(true); const secondChildGrid = childGrids[1]; - const secondChildRow = secondChildGrid.getRowByIndex(0) as IgxHierarchicalRowComponent; + const secondChildRow = secondChildGrid.hgridAPI.get_row_by_index(0) as IgxHierarchicalRowComponent; UIInteractions.simulateClickAndSelectEvent(secondChildRow.expander); fix.detectChanges(); @@ -722,7 +722,7 @@ describe('Excel Exporter', () => { childGrids = hGrid.hgridAPI.getChildGrids(false); const thirdChildGrid = childGrids[3]; - const thirdChildRow = thirdChildGrid.getRowByIndex(0) as IgxHierarchicalRowComponent; + const thirdChildRow = thirdChildGrid.hgridAPI.get_row_by_index(0) as IgxHierarchicalRowComponent; UIInteractions.simulateClickAndSelectEvent(thirdChildRow.expander); fix.detectChanges(); diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts index 7dcce2802cc..c6153697003 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts @@ -94,14 +94,14 @@ export class IgxExcelExporterService extends IgxBaseExporter { .map(a => this._ownersMap.get(a.owner).columns.length + a.level) .sort((a,b) => b - a)[0]; - rootKeys = this._ownersMap.get(firstDataElement.owner).columns.map(c => c.field); + rootKeys = this._ownersMap.get(firstDataElement.owner).columns.map(c => c.header); } else { const defaultOwner = this._ownersMap.get(DEFAULT_OWNER); const columns = defaultOwner.columns.filter(col => !col.skip); columnWidths = defaultOwner.columnWidths; indexOfLastPinnedColumn = defaultOwner.indexOfLastPinnedColumn; columnCount = columns.length; - rootKeys = columns.map(c => !ExportUtilities.isNullOrWhitespaces(c.field) ? c.field : c.header); + rootKeys = columns.map(c => c.header); } } 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 a3263608fff..0a0dac4d0e1 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 @@ -105,6 +105,11 @@ export interface IColumnExportingEventArgs extends IBaseEventArgs { * Export the column's data without applying its formatter, when set to true */ skipFormatter: boolean; + + /** + * A reference to the grid owner. + */ + grid?: IgxGridBaseDirective; } export const DEFAULT_OWNER = 'default'; @@ -177,7 +182,7 @@ export abstract class IgxBaseExporter { } this.prepareData(grid); - this.exportGridRecordsData(this.flatRecords); + this.exportGridRecordsData(this.flatRecords, grid); } /** @@ -208,7 +213,7 @@ export abstract class IgxBaseExporter { this.exportGridRecordsData(records); } - private exportGridRecordsData(records: IExportRecord[]) { + private exportGridRecordsData(records: IExportRecord[], grid?: IgxGridBaseDirective) { if (this._ownersMap.size === 0) { const recordsData = records.map(r => r.data); const keys = ExportUtilities.getKeysFromData(recordsData); @@ -239,7 +244,7 @@ export abstract class IgxBaseExporter { columnIndex: index, cancel: false, skipFormatter: false, - owner: key === DEFAULT_OWNER ? undefined : key + owner: key === DEFAULT_OWNER ? grid : key }; this.columnExporting.emit(columnExportArgs); @@ -406,14 +411,14 @@ export abstract class IgxBaseExporter { }; const islandGrid = grid?.hgridAPI.getChildGrid([path]); - const keyRecordData = this.prepareIslandData(islandGrid, entry[island.key]) || []; + const keyRecordData = this.prepareIslandData(island, islandGrid, entry[island.key]) || []; this.getAllChildColumnsAndData(island, keyRecordData, expansionStateVal, islandGrid); } } } - private prepareIslandData(islandGrid: IgxHierarchicalGridComponent, data: any[]): any[] { + private prepareIslandData(island: IgxRowIslandComponent, islandGrid: IgxHierarchicalGridComponent, data: any[]): any[] { if (islandGrid !== undefined) { const hasFiltering = (islandGrid.filteringExpressionsTree && islandGrid.filteringExpressionsTree.filteringOperands.length > 0) || @@ -446,6 +451,36 @@ export abstract class IgxBaseExporter { data = DataUtil.sort(data, islandGrid.sortingExpressions, islandGrid.sortStrategy, islandGrid); } } + } else { + const hasFiltering = (island.filteringExpressionsTree && + island.filteringExpressionsTree.filteringOperands.length > 0) || + (island.advancedFilteringExpressionsTree && + island.advancedFilteringExpressionsTree.filteringOperands.length > 0); + + const hasSorting = island.sortingExpressions && + island.sortingExpressions.length > 0; + + const skipOperations = + (!hasFiltering || this.options.ignoreFiltering) && + (!hasSorting || this.options.ignoreSorting); + + if (!skipOperations) { + if (hasFiltering && !this.options.ignoreFiltering) { + const filteringState: IFilteringState = { + expressionsTree: island.filteringExpressionsTree, + advancedExpressionsTree: island.advancedFilteringExpressionsTree, + strategy: island.filterStrategy + }; + + data = DataUtil.filter(data, filteringState, island); + } + + if (hasSorting && !this.options.ignoreSorting) { + this._sort = cloneValue(island.sortingExpressions[0]); + + data = DataUtil.sort(data, island.sortingExpressions, island.sortStrategy, island); + } + } } return data; @@ -493,7 +528,7 @@ export abstract class IgxBaseExporter { }; const childIslandGrid = grid?.hgridAPI.getChildGrid([path]); - const keyRecordData = this.prepareIslandData(childIslandGrid, rec[childIsland.key]) || []; + const keyRecordData = this.prepareIslandData(island, childIslandGrid, rec[childIsland.key]) || []; this.getAllChildColumnsAndData(childIsland, keyRecordData, islandExpansionStateVal, childIslandGrid); } From c822ee28314cc2693e02103a05e02d8edd817e6d Mon Sep 17 00:00:00 2001 From: IBarakov Date: Mon, 19 Apr 2021 11:43:11 +0300 Subject: [PATCH 13/15] chore(*): replace property --- .../src/lib/services/exporter-common/base-export-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0a0dac4d0e1..b311a9f8ff0 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 @@ -244,7 +244,7 @@ export abstract class IgxBaseExporter { columnIndex: index, cancel: false, skipFormatter: false, - owner: key === DEFAULT_OWNER ? grid : key + grid: key === DEFAULT_OWNER ? grid : key }; this.columnExporting.emit(columnExportArgs); From 5467f574284979b56e03b0c1fb70ee784c661914 Mon Sep 17 00:00:00 2001 From: IBarakov Date: Mon, 19 Apr 2021 12:04:09 +0300 Subject: [PATCH 14/15] chore(*): edit changelog.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48bd6a6de04..53c52b6cf98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ All notable changes for each version of this project will be documented in this - `onDataPreLoad` -> `dataPreLoad` ### New Features +- `IgxHierarchicalGrid` + - Added support for exporting hierarchical data. - `IgxForOf`, `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid` - **Behavioral Change** - Virtual containers now scroll smoothly when using the mouse wheel(s) to scroll them horizontally or vertically. This behavior more closely resembles the scrolling behavior of non-virtualized containers in most modern browsers. - `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid` From 626dea1a25a7f538d7359d2dba29f18ded8830b6 Mon Sep 17 00:00:00 2001 From: IBarakov Date: Mon, 19 Apr 2021 12:21:28 +0300 Subject: [PATCH 15/15] chore(*): remove leftover comment --- .../src/lib/services/exporter-common/base-export-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b311a9f8ff0..809cec9c4a5 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 @@ -161,7 +161,7 @@ export abstract class IgxBaseExporter { if (options === undefined || options === null) { throw Error('No options provided!'); } - //options.ignoreSorting = true; + this.options = options; const columns = grid.columnList.toArray();