From 808785e0ea9508f817453211d8ed808398aa9c01 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Tue, 21 Apr 2020 18:50:05 -0400 Subject: [PATCH] feat(package): add new Excel Export package --- .vscode/launch.json | 2 + packages/common/src/global-grid-options.ts | 26 +- .../services/__tests__/filter.service.spec.ts | 1 + packages/excel-export/README.md | 7 + packages/excel-export/package.json | 47 + .../src/excelExport.service.spec.ts | 1288 +++++++++++++++++ .../excel-export/src/excelExport.service.ts | 484 +++++++ packages/excel-export/src/index.ts | 1 + .../interfaces/excelCellFormat.interface.ts | 6 + .../excelCopyBufferOption.interface.ts | 55 + .../interfaces/excelExportOption.interface.ts | 44 + .../src/interfaces/excelMetadata.interface.ts | 9 + .../interfaces/excelStylesheet.interface.ts | 32 + .../src/interfaces/excelWorkbook.interface.ts | 17 + .../interfaces/excelWorksheet.interface.ts | 36 + packages/excel-export/src/interfaces/index.ts | 7 + .../excel-builder-webpacker/index.d.ts | 1 + packages/excel-export/src/typings/global.d.ts | 1 + .../excel-export/src/typings/typings.d.ts | 12 + packages/excel-export/tsconfig.build.json | 46 + packages/excel-export/tsconfig.json | 47 + packages/export/src/export.service.ts | 2 - .../src/services/excelExport.service.ts | 12 +- .../vanilla-bundle/src/vanilla-grid-bundle.ts | 11 +- 24 files changed, 2172 insertions(+), 22 deletions(-) create mode 100644 packages/excel-export/README.md create mode 100644 packages/excel-export/package.json create mode 100644 packages/excel-export/src/excelExport.service.spec.ts create mode 100644 packages/excel-export/src/excelExport.service.ts create mode 100644 packages/excel-export/src/index.ts create mode 100644 packages/excel-export/src/interfaces/excelCellFormat.interface.ts create mode 100644 packages/excel-export/src/interfaces/excelCopyBufferOption.interface.ts create mode 100644 packages/excel-export/src/interfaces/excelExportOption.interface.ts create mode 100644 packages/excel-export/src/interfaces/excelMetadata.interface.ts create mode 100644 packages/excel-export/src/interfaces/excelStylesheet.interface.ts create mode 100644 packages/excel-export/src/interfaces/excelWorkbook.interface.ts create mode 100644 packages/excel-export/src/interfaces/excelWorksheet.interface.ts create mode 100644 packages/excel-export/src/interfaces/index.ts create mode 100644 packages/excel-export/src/typings/excel-builder-webpacker/index.d.ts create mode 100644 packages/excel-export/src/typings/global.d.ts create mode 100644 packages/excel-export/src/typings/typings.d.ts create mode 100644 packages/excel-export/tsconfig.build.json create mode 100644 packages/excel-export/tsconfig.json diff --git a/.vscode/launch.json b/.vscode/launch.json index c6a977fd1..db2834cc3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,6 +15,8 @@ "trace": true, "sourceMapPathOverrides": { "webpack:///../common/*": "${webRoot}/packages/common/*", + "webpack:///../excel-export/*": "${webRoot}/packages/excel-export/*", + "webpack:///../export/*": "${webRoot}/packages/export/*", "webpack:///../vanilla-bundle/*": "${webRoot}/packages/vanilla-bundle/*", "webpack:///./src/*": "${webRoot}/packages/web-demo-vanilla-bundle/src/*" } diff --git a/packages/common/src/global-grid-options.ts b/packages/common/src/global-grid-options.ts index 823d32fcd..b6bef471d 100644 --- a/packages/common/src/global-grid-options.ts +++ b/packages/common/src/global-grid-options.ts @@ -91,25 +91,25 @@ export const GlobalGridOptions: GridOption = { enableColumnPicker: true, enableColumnReorder: true, enableContextMenu: true, - // enableExcelExport: true, // Excel Export is the new default, - enableExport: true, // CSV/Text with Tab Delimited + enableExcelExport: false, + enableExport: false, enableGridMenu: true, enableHeaderMenu: true, enableMouseHoverHighlightRow: true, enableSorting: true, enableTextSelectionOnCells: true, explicitInitialization: true, - // // excelExportOptions: { - // // addGroupIndentation: true, - // // exportWithFormatter: false, - // // filename: 'export', - // // format: FileType.xlsx, - // // groupingColumnHeaderTitle: 'Group By', - // // groupCollapsedSymbol: '\u25B9', - // // groupExpandedSymbol: '\u25BF', - // // groupingAggregatorRowText: '', - // // sanitizeDataExport: false, - // // }, + excelExportOptions: { + addGroupIndentation: true, + exportWithFormatter: false, + filename: 'export', + format: FileType.xlsx, + groupingColumnHeaderTitle: 'Group By', + groupCollapsedSymbol: '\u25B9', + groupExpandedSymbol: '\u25BF', + groupingAggregatorRowText: '', + sanitizeDataExport: false, + }, exportOptions: { delimiter: DelimiterType.comma, exportWithFormatter: false, diff --git a/packages/common/src/services/__tests__/filter.service.spec.ts b/packages/common/src/services/__tests__/filter.service.spec.ts index 86f8b364b..f18252502 100644 --- a/packages/common/src/services/__tests__/filter.service.spec.ts +++ b/packages/common/src/services/__tests__/filter.service.spec.ts @@ -77,6 +77,7 @@ const pubSubServiceStub = { publish: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn(), + unsubscribeAll: jest.fn(), } as PubSubService; describe('FilterService', () => { diff --git a/packages/excel-export/README.md b/packages/excel-export/README.md new file mode 100644 index 000000000..9805ab848 --- /dev/null +++ b/packages/excel-export/README.md @@ -0,0 +1,7 @@ +## Vanilla Bundle +#### @slickgrid-universal/export + +Simple Export to Excel Service that allows to exporting as ".xls" or ".xlsx". + +### Installation +Follow the instruction provided in the main [README](https://github.com/ghiscoding/slickgrid-universal#installation), you can see a demo by looking at the [GitHub Demo](https://ghiscoding.github.io/slickgrid-universal) page and click on "Export to CSV" from the Grid Menu (aka hamburger menu). diff --git a/packages/excel-export/package.json b/packages/excel-export/package.json new file mode 100644 index 000000000..46e396564 --- /dev/null +++ b/packages/excel-export/package.json @@ -0,0 +1,47 @@ +{ + "name": "@slickgrid-universal/excel-export", + "version": "0.0.2", + "description": "Excel Export (xls/xlsx) Service.", + "browser": "src/index.ts", + "main": "dist/es2020/index.js", + "typings": "dist/es2020/index.d.ts", + "files": [ + "src", + "dist" + ], + "scripts": { + "build": "cross-env tsc --build", + "build:watch": "cross-env tsc --incremental --watch", + "dev": "run-s build sass:build sass:copy", + "dev:watch": "run-p build:watch", + "bundle:commonjs": "tsc --project tsconfig.build.json --outDir dist/commonjs --module commonjs", + "bundle:es2020": "cross-env tsc --project tsconfig.build.json --outDir dist/es2020 --module es2015 --target es2020", + "bundle": "npm-run-all bundle:commonjs bundle:es2020", + "prebundle": "npm-run-all delete:dist", + "delete:dist": "cross-env rimraf dist" + }, + "author": "Ghislain B.", + "license": "MIT", + "engines": { + "node": ">=12.13.1", + "npm": ">=6.12.1" + }, + "browserslist": [ + "last 2 version", + "> 1%", + "maintained node versions", + "not dead", + "IE 11" + ], + "dependencies": { + "@slickgrid-universal/common": "^0.0.2", + "excel-builder-webpacker": "^1.0.5", + "moment-mini": "^2.24.0" + }, + "devDependencies": { + "cross-env": "^7.0.2", + "npm-run-all": "^4.1.5", + "rimraf": "^3.0.2", + "typescript": "^3.8.3" + } +} diff --git a/packages/excel-export/src/excelExport.service.spec.ts b/packages/excel-export/src/excelExport.service.spec.ts new file mode 100644 index 000000000..22a995373 --- /dev/null +++ b/packages/excel-export/src/excelExport.service.spec.ts @@ -0,0 +1,1288 @@ +import { ExcelExportService } from './excelExport.service'; +import { + Column, + ExcelExportOption, + FieldType, + FileType, + Formatter, + Formatters, + GridOption, + GroupTotalsFormatter, + GroupTotalFormatters, + SortDirectionNumber, + SortComparers, + PubSubService, +} from '@slickgrid-universal/common'; +import moment = require('moment-mini'); +import { TranslateServiceStub } from '../../../test/translateServiceStub'; + +function removeMultipleSpaces(textS) { + return `${textS}`.replace(/ +/g, ''); +} + +const pubSubServiceStub = { + publish: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + unsubscribeAll: jest.fn(), +} as PubSubService; + +// URL object is not supported in JSDOM, we can simply mock it +(global as any).URL.createObjectURL = jest.fn(); + +const myBoldHtmlFormatter: Formatter = (row, cell, value, columnDef, dataContext) => value !== null ? { text: `${value}` } : null; +const myUppercaseFormatter: Formatter = (row, cell, value, columnDef, dataContext) => value ? { text: value.toUpperCase() } : null; +const myUppercaseGroupTotalFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column) => { + const field = columnDef.field || ''; + const val = totals.sum && totals.sum[field]; + if (val != null && !isNaN(+val)) { + return `Custom: ${val}`; + } + return ''; +}; +const myCustomObjectFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid: any) => { + let textValue = value && value.hasOwnProperty('text') ? value.text : value; + const toolTip = value && value.hasOwnProperty('toolTip') ? value.toolTip : ''; + const cssClasses = value && value.hasOwnProperty('addClasses') ? [value.addClasses] : ['']; + if (dataContext && !isNaN(dataContext.order) && parseFloat(dataContext.order) > 10) { + cssClasses.push('red'); + textValue = null; + } + return { text: textValue, addClasses: cssClasses.join(' '), toolTip }; +}; + +const dataViewStub = { + getGrouping: jest.fn(), + getItem: jest.fn(), + getLength: jest.fn(), + setGrouping: jest.fn(), +}; + +const mockGridOptions = { + enableExcelExport: true, + enablePagination: true, + enableFiltering: true, +} as GridOption; + +const gridStub = { + getColumnIndex: jest.fn(), + getOptions: () => mockGridOptions, + getColumns: jest.fn(), + getGrouping: jest.fn(), +}; + +describe('ExcelExportService', () => { + let pubSubService: PubSubService; + let service: ExcelExportService; + let translateService: TranslateServiceStub; + let mockColumns: Column[]; + let mockExcelBlob: Blob; + let mockExportExcelOptions: ExcelExportOption; + + describe('with I18N Service', () => { + beforeEach(() => { + translateService = new TranslateServiceStub(); + + // @ts-ignore + navigator.__defineGetter__('appName', () => 'Netscape'); + navigator.msSaveOrOpenBlob = undefined; + mockExcelBlob = new Blob(['', ''], { type: `text/xlsx;charset=utf-8;` }); + + mockExportExcelOptions = { + filename: 'export', + format: FileType.xlsx, + }; + + service = new ExcelExportService(pubSubServiceStub, translateService); + }); + + afterEach(() => { + delete mockGridOptions.backendServiceApi; + jest.clearAllMocks(); + }); + + it('should create the service', () => { + expect(service).toBeTruthy(); + expect(document).toBeTruthy(); + }); + + it('should not have any output since there are no column definitions provided', async () => { + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: FileType.xlsx }; + + service.init(gridStub, dataViewStub); + const result = await service.exportToExcel(mockExportExcelOptions); + + expect(result).toBeTruthy(); + expect(pluginEaSpy).toHaveBeenNthCalledWith(1, `onBeforeExportToExcel`, true); + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ ...optionExpectation, blob: new Blob(), data: [[]] }); + }); + + describe('exportToExcel method', () => { + beforeEach(() => { + mockColumns = [ + { id: 'id', field: 'id', excludeFromExport: true }, + { id: 'userId', field: 'userId', name: 'User Id', width: 100, exportCsvForceToKeepAsString: true }, + { id: 'firstName', field: 'firstName', width: 100, formatter: myBoldHtmlFormatter }, + { id: 'lastName', field: 'lastName', width: 100, formatter: myBoldHtmlFormatter, exportCustomFormatter: myUppercaseFormatter, sanitizeDataExport: true, exportWithFormatter: true }, + { id: 'position', field: 'position', width: 100 }, + { id: 'order', field: 'order', width: 100, exportWithFormatter: true, formatter: Formatters.multiple, params: { formatters: [myBoldHtmlFormatter, myCustomObjectFormatter] } }, + ] as Column[]; + + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + }); + + it('should throw an error when trying call exportToExcel" without a grid and/or dataview object initialized', async () => { + try { + service.init(null, null); + await service.exportToExcel(mockExportExcelOptions); + } catch (e) { + expect(e.toString()).toContain('[Aurelia-Slickgrid] it seems that the SlickGrid & DataView objects are not initialized did you forget to enable the grid option flag "enableExcelExport"?'); + } + }); + + it('should trigger an event before exporting the file', async () => { + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + + service.init(gridStub, dataViewStub); + const result = await service.exportToExcel(mockExportExcelOptions); + + expect(result).toBeTruthy(); + expect(pluginEaSpy).toHaveBeenNthCalledWith(1, `onBeforeExportToExcel`, true); + }); + + it('should trigger an event after exporting the file', async () => { + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + + service.init(gridStub, dataViewStub); + const result = await service.exportToExcel(mockExportExcelOptions); + + expect(result).toBeTruthy(); + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, expect.anything()); + + }); + + it('should call "URL.createObjectURL" with a Blob and xlsx file when browser is not IE11 (basically any other browser) when exporting as xlsx', async () => { + const optionExpectation = { filename: 'export.xlsx', format: FileType.xlsx }; + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + + service.init(gridStub, dataViewStub); + const result = await service.exportToExcel(mockExportExcelOptions); + + expect(result).toBeTruthy(); + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + }); + + + it('should call "msSaveOrOpenBlob" with a Blob and xlsx file when browser is IE11 when exporting as xlsx', async () => { + const optionExpectation = { filename: 'export.xlsx', format: FileType.xlsx }; + navigator.msSaveOrOpenBlob = jest.fn(); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyMsSave = jest.spyOn(navigator, 'msSaveOrOpenBlob'); + + service.init(gridStub, dataViewStub); + const result = await service.exportToExcel(mockExportExcelOptions); + + expect(result).toBeTruthy(); + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyMsSave).toHaveBeenCalledWith(mockExcelBlob, 'export.xlsx'); + }); + + it('should throw an error when browser is IE10 or lower', async () => { + // @ts-ignore + navigator.__defineGetter__('appName', () => 'Microsoft Internet Explorer'); + + try { + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + } catch (e) { + expect(e.toString()).toContain('Microsoft Internet Explorer 6 to 10 do not support javascript export to Excel.'); + } + }); + }); + + describe('startDownloadFile call after all private methods ran ', () => { + let mockCollection: any[]; + + it(`should have the Order exported correctly with multiple formatters which have 1 of them returning an object with a text property (instead of simple string)`, async () => { + mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: FileType.xlsx }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'FirstName', }, + { metadata: { style: 1, }, value: 'LastName', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['1E06', 'John', 'Z', 'SALES_REP', '10'], + ] + }); + }); + + it(`should have the LastName in uppercase when "formatter" is defined but also has "exportCustomFormatter" which will be used`, async () => { + mockCollection = [{ id: 1, userId: '2B02', firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'FirstName', }, + { metadata: { style: 1, }, value: 'LastName', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', '1'], + ] + }); + }); + + it(`should have the LastName as empty string when item LastName is NULL and column definition "formatter" is defined but also has "exportCustomFormatter" which will be used`, async () => { + mockCollection = [{ id: 2, userId: '3C2', firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 3 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'FirstName', }, + { metadata: { style: 1, }, value: 'LastName', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['3C2', 'Ava Luna', '', 'HUMAN_RESOURCES', '3'], + ] + }); + }); + + it(`should have the UserId as empty string even when UserId property is not found in the item object`, async () => { + mockCollection = [{ id: 2, firstName: 'Ava', lastName: 'Luna', position: 'HUMAN_RESOURCES', order: 3 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'FirstName', }, + { metadata: { style: 1, }, value: 'LastName', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['', 'Ava', 'LUNA', 'HUMAN_RESOURCES', '3'], + ] + }); + }); + + it(`should have the Order as empty string when using multiple formatters and last one result in a null output because its value is bigger than 10`, async () => { + mockCollection = [{ id: 2, userId: '3C2', firstName: 'Ava', lastName: 'Luna', position: 'HUMAN_RESOURCES', order: 13 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'FirstName', }, + { metadata: { style: 1, }, value: 'LastName', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['3C2', 'Ava', 'LUNA', 'HUMAN_RESOURCES', ''], + ] + }); + }); + + it(`should have the UserId as empty string when its input value is null`, async () => { + mockCollection = [{ id: 3, userId: undefined, firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 },]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'FirstName', }, + { metadata: { style: 1, }, value: 'LastName', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['', '', 'CASH', 'SALES_REP', '3'], + ] + }); + }); + + it(`should have the Order without html tags when the grid option has "sanitizeDataExport" is enabled`, async () => { + mockGridOptions.excelExportOptions = { sanitizeDataExport: true }; + mockCollection = [{ id: 1, userId: '2B02', firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'FirstName', }, + { metadata: { style: 1, }, value: 'LastName', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', '1'], + ] + }); + }); + + it(`should have different styling for header titles when the grid option has "columnHeaderStyle" provided with custom styles`, async () => { + mockGridOptions.excelExportOptions = { columnHeaderStyle: { font: { bold: true, italic: true } } }; + mockCollection = [{ id: 1, userId: '2B02', firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 5, }, value: 'User Id', }, + { metadata: { style: 5, }, value: 'FirstName', }, + { metadata: { style: 5, }, value: 'LastName', }, + { metadata: { style: 5, }, value: 'Position', }, + { metadata: { style: 5, }, value: 'Order', }, + ], + ['2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', '1'], + ] + }); + }); + + it(`should have a custom Title when "customExcelHeader" is provided`, async () => { + mockGridOptions.excelExportOptions = { + sanitizeDataExport: true, + customExcelHeader: (workbook, sheet) => { + const stylesheet = workbook.getStyleSheet(); + const aFormatDefn = { + 'font': { 'size': 12, 'fontName': 'Calibri', 'bold': true, color: 'FF0000FF' }, // every color starts with FF, then regular HTML color + 'alignment': { 'wrapText': true } + }; + const formatterId = stylesheet.createFormat(aFormatDefn); + sheet.setRowInstructions(0, { height: 30 }); // change height of row 0 + + // excel cells start with A1 which is upper left corner + sheet.mergeCells('B1', 'D1'); + const cols = []; + // push empty data on A1 + cols.push({ value: '' }); + // push data in B1 cell with metadata formatter + cols.push({ value: 'My header that is long enough to wrap', metadata: { style: formatterId.id } }); + sheet.data.push(cols); + } + }; + mockCollection = [{ id: 1, userId: '2B02', firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { value: '' }, + { metadata: { style: 5, }, value: 'My header that is long enough to wrap', } + ], + [ + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'FirstName', }, + { metadata: { style: 1, }, value: 'LastName', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', '1'], + ] + }); + }); + }); + + describe('exportToExcel method with Date Fields', () => { + let mockCollection: any[]; + + beforeEach(() => { + mockGridOptions.excelExportOptions = { sanitizeDataExport: true }; + mockColumns = [ + { id: 'id', field: 'id', excludeFromExport: true }, + { id: 'userId', field: 'userId', name: 'User Id', width: 100 }, + { id: 'firstName', field: 'firstName', width: 100, formatter: myBoldHtmlFormatter }, + { id: 'lastName', field: 'lastName', width: 100, sanitizeDataExport: true, exportWithFormatter: true }, + { id: 'position', field: 'position', width: 100 }, + { id: 'startDate', field: 'startDate', type: FieldType.dateIso, width: 100, exportWithFormatter: false }, + { id: 'endDate', field: 'endDate', width: 100, formatter: Formatters.dateIso, type: FieldType.dateUtc, exportWithFormatter: true, outputType: FieldType.dateIso }, + ] as Column[]; + + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + }); + + + it(`should expect Date exported correctly when Field Type is provided and we use "exportWithFormatter" set to True & False`, async () => { + mockCollection = [ + { id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', startDate: '2005-12-20T18:19:19.992Z', endDate: null }, + { id: 1, userId: '1E09', firstName: 'Jane', lastName: 'Doe', position: 'HUMAN_RESOURCES', startDate: '2010-10-09T18:19:19.992Z', endDate: '2024-01-02T16:02:02.000Z' }, + ]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]).mockReturnValueOnce(mockCollection[1]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: FileType.xlsx }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'FirstName', }, + { metadata: { style: 1, }, value: 'LastName', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'StartDate', }, + { metadata: { style: 1, }, value: 'EndDate', }, + ], + ['1E06', 'John', 'Z', 'SALES_REP', { metadata: { style: 5, }, value: '2005-12-20' }, ''], + ['1E09', 'Jane', 'Doe', 'HUMAN_RESOURCES', { metadata: { style: 6, }, value: '2010-10-09' }, '2024-01-02'], + ] + }); + }); + }); + + describe('startDownloadFile with some columns having complex object', () => { + beforeEach(() => { + mockGridOptions.excelExportOptions = { sanitizeDataExport: true }; + mockColumns = [ + { id: 'id', field: 'id', excludeFromExport: true }, + { id: 'firstName', field: 'user.firstName', name: 'First Name', width: 100, formatter: Formatters.complexObject, exportWithFormatter: true }, + { id: 'lastName', field: 'user.lastName', name: 'Last Name', width: 100, formatter: Formatters.complexObject, exportWithFormatter: true }, + { id: 'position', field: 'position', width: 100 }, + ] as Column[]; + + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + }); + + let mockCollection: any[]; + + it(`should export correctly with complex object formatters`, async () => { + mockCollection = [{ id: 0, user: { firstName: 'John', lastName: 'Z' }, position: 'SALES_REP', order: 10 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'First Name', }, + { metadata: { style: 1, }, value: 'Last Name', }, + { metadata: { style: 1, }, value: 'Position', }, + ], + ['John', 'Z', 'SALES_REP'], + ] + }); + }); + }); + + describe('with Translation', () => { + let mockCollection: any[]; + + beforeEach(() => { + mockGridOptions.enableTranslate = true; + mockGridOptions.i18n = translateService; + + mockColumns = [ + { id: 'id', field: 'id', excludeFromExport: true }, + { id: 'userId', field: 'userId', name: 'User Id', width: 100 }, + { id: 'firstName', nameKey: 'FIRST_NAME', width: 100, formatter: myBoldHtmlFormatter }, + { id: 'lastName', field: 'lastName', nameKey: 'LAST_NAME', width: 100, formatter: myBoldHtmlFormatter, exportCustomFormatter: myUppercaseFormatter, sanitizeDataExport: true, exportWithFormatter: true }, + { id: 'position', field: 'position', name: 'Position', width: 100, formatter: Formatters.translate, exportWithFormatter: true }, + { id: 'order', field: 'order', width: 100, exportWithFormatter: true, formatter: Formatters.multiple, params: { formatters: [myBoldHtmlFormatter, myCustomObjectFormatter] } }, + ] as Column[]; + + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + }); + + it(`should have the LastName header title translated when defined as a "nameKey" and "i18n" is set in grid option`, async () => { + mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'First Name', }, + { metadata: { style: 1, }, value: 'Last Name', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['1E06', 'John', 'Z', 'Sales Rep.', '10'], + ] + }); + }); + }); + + describe('with Grouping', () => { + let mockCollection: any[]; + let mockOrderGrouping; + let mockItem1; + let mockItem2; + let mockGroup1; + + beforeEach(() => { + mockGridOptions.enableGrouping = true; + mockGridOptions.enableTranslate = false; + mockGridOptions.excelExportOptions = { sanitizeDataExport: true, addGroupIndentation: true }; + + mockColumns = [ + { id: 'id', field: 'id', excludeFromExport: true }, + { id: 'userId', field: 'userId', name: 'User Id', width: 100 }, + { id: 'firstName', field: 'firstName', width: 100, formatter: myBoldHtmlFormatter }, + { id: 'lastName', field: 'lastName', width: 100, formatter: myBoldHtmlFormatter, exportCustomFormatter: myUppercaseFormatter, sanitizeDataExport: true, exportWithFormatter: true }, + { id: 'position', field: 'position', width: 100 }, + { + id: 'order', field: 'order', type: FieldType.number, + exportWithFormatter: true, + formatter: Formatters.multiple, params: { formatters: [myBoldHtmlFormatter, myCustomObjectFormatter] }, + groupTotalsFormatter: GroupTotalFormatters.sumTotals, + exportCustomGroupTotalsFormatter: myUppercaseGroupTotalFormatter, + }, + ] as Column[]; + + mockOrderGrouping = { + aggregateChildGroups: false, + aggregateCollapsed: false, + aggregateEmpty: false, + aggregators: [{ _count: 2, _field: 'order', _nonNullCount: 2, _sum: 4, }], + collapsed: false, + comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.asc), + compiledAccumulators: [jest.fn(), jest.fn()], + displayTotalsRow: true, + formatter: (g) => `Order: ${g.value} (${g.count} items)`, + getter: 'order', + getterIsAFn: false, + lazyTotalsCalculation: true, + predefinedValues: [], + }; + + mockItem1 = { id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }; + mockItem2 = { id: 1, userId: '2B02', firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 10 }; + mockGroup1 = { + collapsed: 0, count: 2, groupingKey: '10', groups: null, level: 0, selectChecked: false, + rows: [mockItem1, mockItem2], + title: `Order: 20 (2 items)`, + totals: { value: '10', __group: true, __groupTotals: true, group: {}, initialized: true, sum: { order: 20 } }, + }; + + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + mockCollection = [mockGroup1, mockItem1, mockItem2, { __groupTotals: true, initialized: true, sum: { order: 20 }, group: mockGroup1 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem') + .mockReturnValue(null) + .mockReturnValueOnce(mockCollection[0]) + .mockReturnValueOnce(mockCollection[1]) + .mockReturnValueOnce(mockCollection[2]) + .mockReturnValueOnce(mockCollection[3]); + jest.spyOn(dataViewStub, 'getGrouping').mockReturnValue([mockOrderGrouping]); + }); + + it(`should have a xlsx export with grouping (same as the grid, WYSIWYG) when "enableGrouping" is set in the grid options and grouping are defined`, async () => { + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'Group By', }, + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'FirstName', }, + { metadata: { style: 1, }, value: 'LastName', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['▿ Order: 20 (2 items)'], + ['', '1E06', 'John', 'Z', 'SALES_REP', '10'], + ['', '2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', '10'], + ['', '', '', '', '', 'Custom: 20'], + ] + }); + }); + }); + + describe('with Grouping and Translation', () => { + let mockCollection: any[]; + let mockOrderGrouping; + let mockItem1; + let mockItem2; + let mockGroup1; + + beforeEach(() => { + mockGridOptions.enableGrouping = true; + mockGridOptions.enableTranslate = true; + mockGridOptions.excelExportOptions = { sanitizeDataExport: true, addGroupIndentation: true }; + + mockColumns = [ + { id: 'id', field: 'id', excludeFromExport: true }, + { id: 'userId', field: 'userId', name: 'User Id', width: 100 }, + { id: 'firstName', field: 'firstName', nameKey: 'FIRST_NAME', width: 100, formatter: myBoldHtmlFormatter }, + { id: 'lastName', field: 'lastName', nameKey: 'LAST_NAME', width: 100, formatter: myBoldHtmlFormatter, exportCustomFormatter: myUppercaseFormatter, sanitizeDataExport: true, exportWithFormatter: true }, + { id: 'position', field: 'position', name: 'Position', width: 100, formatter: Formatters.translate, exportWithFormatter: true }, + { + id: 'order', field: 'order', type: FieldType.number, + exportWithFormatter: true, + formatter: Formatters.multiple, params: { formatters: [myBoldHtmlFormatter, myCustomObjectFormatter] }, + groupTotalsFormatter: GroupTotalFormatters.sumTotals, + }, + ] as Column[]; + + mockOrderGrouping = { + aggregateChildGroups: false, + aggregateCollapsed: false, + aggregateEmpty: false, + aggregators: [{ _count: 2, _field: 'order', _nonNullCount: 2, _sum: 4, }], + collapsed: false, + comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.asc), + compiledAccumulators: [jest.fn(), jest.fn()], + displayTotalsRow: true, + formatter: (g) => `Order: ${g.value} (${g.count} items)`, + getter: 'order', + getterIsAFn: false, + lazyTotalsCalculation: true, + predefinedValues: [], + }; + + mockItem1 = { id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }; + mockItem2 = { id: 1, userId: '2B02', firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 10 }; + mockGroup1 = { + collapsed: 0, count: 2, groupingKey: '10', groups: null, level: 0, selectChecked: false, + rows: [mockItem1, mockItem2], + title: `Order: 20 (2 items)`, + totals: { value: '10', __group: true, __groupTotals: true, group: {}, initialized: true, sum: { order: 20 } }, + }; + + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + mockCollection = [mockGroup1, mockItem1, mockItem2, { __groupTotals: true, initialized: true, sum: { order: 20 }, group: mockGroup1 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem') + .mockReturnValue(null) + .mockReturnValueOnce(mockCollection[0]) + .mockReturnValueOnce(mockCollection[1]) + .mockReturnValueOnce(mockCollection[2]) + .mockReturnValueOnce(mockCollection[3]); + jest.spyOn(dataViewStub, 'getGrouping').mockReturnValue([mockOrderGrouping]); + }); + + it(`should have a xlsx export with grouping (same as the grid, WYSIWYG) when "enableGrouping" is set in the grid options and grouping are defined`, async () => { + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'Grouped By', }, + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'First Name', }, + { metadata: { style: 1, }, value: 'Last Name', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['▿ Order: 20 (2 items)'], + ['', '1E06', 'John', 'Z', 'Sales Rep.', '10'], + ['', '2B02', 'Jane', 'DOE', 'Finance Manager', '10'], + ['', '', '', '', '', '20'], + ] + }); + }); + }); + + describe('with Multiple Columns Grouping (by Order then by LastName) and Translation', () => { + let mockCollection: any[]; + let mockOrderGrouping; + let mockLastNameGrouping; + let mockItem1; + let mockItem2; + let mockGroup1; + let mockGroup2; + let mockGroup3; + let mockGroup4; + + beforeEach(() => { + mockGridOptions.enableGrouping = true; + mockGridOptions.enableTranslate = true; + mockGridOptions.excelExportOptions = { sanitizeDataExport: true, addGroupIndentation: true, exportWithFormatter: true }; + + mockColumns = [ + { id: 'id', field: 'id', excludeFromExport: true }, + { id: 'userId', field: 'userId', name: 'User Id', width: 100 }, + { id: 'firstName', field: 'firstName', nameKey: 'FIRST_NAME', width: 100, formatter: myBoldHtmlFormatter }, + { id: 'lastName', field: 'lastName', nameKey: 'LAST_NAME', width: 100, formatter: myBoldHtmlFormatter, exportCustomFormatter: myUppercaseFormatter, sanitizeDataExport: true, exportWithFormatter: true }, + { id: 'position', field: 'position', name: 'Position', width: 100, formatter: Formatters.translate }, + { + id: 'order', field: 'order', type: FieldType.number, + exportWithFormatter: true, + formatter: Formatters.multiple, params: { formatters: [myBoldHtmlFormatter, myCustomObjectFormatter] }, + groupTotalsFormatter: GroupTotalFormatters.sumTotals, + }, + ] as Column[]; + + mockOrderGrouping = { + aggregateChildGroups: false, + aggregateCollapsed: false, + aggregateEmpty: false, + aggregators: [{ _count: 2, _field: 'order', _nonNullCount: 2, _sum: 4, }], + collapsed: false, + comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.asc), + compiledAccumulators: [jest.fn(), jest.fn()], + displayTotalsRow: true, + formatter: (g) => `Order: ${g.value} (${g.count} items)`, + getter: 'order', + getterIsAFn: false, + lazyTotalsCalculation: true, + predefinedValues: [], + }; + + mockLastNameGrouping = { + aggregateChildGroups: false, + aggregateCollapsed: false, + aggregateEmpty: false, + aggregators: [{ _count: 1, _field: 'lastName', _nonNullCount: 2, _sum: 4, }], + collapsed: false, + comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.asc), + compiledAccumulators: [jest.fn(), jest.fn()], + displayTotalsRow: true, + formatter: (g) => `Last Name: ${g.value} (${g.count} items)`, + getter: 'lastName', + getterIsAFn: false, + lazyTotalsCalculation: true, + predefinedValues: [], + }; + + mockItem1 = { id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }; + mockItem2 = { id: 1, userId: '2B02', firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 10 }; + mockGroup1 = { + collapsed: false, count: 2, groupingKey: '10', groups: null, level: 0, selectChecked: false, + rows: [mockItem1, mockItem2], + title: `Order: 20 (2 items)`, + totals: { value: '10', __group: true, __groupTotals: true, group: {}, initialized: true, sum: { order: 20 } }, + }; + mockGroup2 = { + collapsed: false, count: 2, groupingKey: '10:|:Z', groups: null, level: 1, selectChecked: false, + rows: [mockItem1, mockItem2], + title: `Last Name: Z (1 items)`, + totals: { value: '10', __group: true, __groupTotals: true, group: {}, initialized: true, sum: { order: 10 } }, + }; + mockGroup3 = { + collapsed: false, count: 2, groupingKey: '10:|:Doe', groups: null, level: 1, selectChecked: false, + rows: [mockItem1, mockItem2], + title: `Last Name: Doe (1 items)`, + totals: { value: '10', __group: true, __groupTotals: true, group: {}, initialized: true, sum: { order: 10 } }, + }; + mockGroup4 = { + collapsed: true, count: 0, groupingKey: '10:|:', groups: null, level: 1, selectChecked: false, + rows: [], + title: `Last Name: null (0 items)`, + totals: { value: '0', __group: true, __groupTotals: true, group: {}, initialized: true, sum: { order: 10 } }, + }; + + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + mockCollection = [ + mockGroup1, mockGroup2, mockItem1, mockGroup3, mockItem2, mockGroup4, + { __groupTotals: true, initialized: true, sum: { order: 20 }, group: mockGroup1 }, + { __groupTotals: true, initialized: true, sum: { order: 10 }, group: mockGroup2 }, + ]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem') + .mockReturnValue(null) + .mockReturnValueOnce(mockCollection[0]) + .mockReturnValueOnce(mockCollection[1]) + .mockReturnValueOnce(mockCollection[2]) + .mockReturnValueOnce(mockCollection[3]) + .mockReturnValueOnce(mockCollection[4]) + .mockReturnValueOnce(mockCollection[5]) + .mockReturnValueOnce(mockCollection[6]) + .mockReturnValueOnce(mockCollection[7]); + jest.spyOn(dataViewStub, 'getGrouping').mockReturnValue([mockOrderGrouping]); + }); + + it(`should have a xlsx export with grouping (same as the grid, WYSIWYG) when "enableGrouping" is set in the grid options and grouping are defined`, async () => { + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'Grouped By', }, + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'First Name', }, + { metadata: { style: 1, }, value: 'Last Name', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['▿ Order: 20 (2 items)'], + ['▿ Last Name: Z (1 items)'], // expanded + ['', '1E06', 'John', 'Z', 'Sales Rep.', '10'], + ['▿ Last Name: Doe (1 items)'], // expanded + ['', '2B02', 'Jane', 'DOE', 'Finance Manager', '10'], + ['▹ Last Name: null (0 items)'], // collapsed + ['', '', '', '', '', '20'], + ['', '', '', '', '', '10'], + ] + }); + }); + + it(`should have a xlsx export with grouping but without indentation when "addGroupIndentation" is set to False + and field should be exported as metadata when "exportWithFormatter" is false and the field type is number`, async () => { + mockColumns[5].exportWithFormatter = false; // "order" field that is of type number will be exported as a number cell format metadata + mockGridOptions.excelExportOptions.addGroupIndentation = false; + const pluginEaSpy = jest.spyOn(pubSubService, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + + expect(pluginEaSpy).toHaveBeenNthCalledWith(2, `onAfterExportToExcel`, optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob); + expect(spyDownload).toHaveBeenCalledWith({ + ...optionExpectation, blob: new Blob(), data: [ + [ + { metadata: { style: 1, }, value: 'Grouped By', }, + { metadata: { style: 1, }, value: 'User Id', }, + { metadata: { style: 1, }, value: 'First Name', }, + { metadata: { style: 1, }, value: 'Last Name', }, + { metadata: { style: 1, }, value: 'Position', }, + { metadata: { style: 1, }, value: 'Order', }, + ], + ['Order: 20 (2 items)'], + ['Last Name: Z (1 items)'], + ['', '1E06', 'John', 'Z', 'Sales Rep.', { metadata: { style: 3 }, value: '10', }], + ['Last Name: Doe (1 items)'], + ['', '2B02', 'Jane', 'DOE', 'Finance Manager', { metadata: { style: 3 }, value: '10', }], + ['Last Name: null (0 items)'], + ['', '', '', '', '', '20'], + ['', '', '', '', '', '10'], + ] + }); + }); + }); + + describe('useCellFormatByFieldType method', () => { + it('should return a date time format when using FieldType.dateTime and a Date object as input', async () => { + const input = new Date('2012-02-28 15:07:59'); + const expectedDate = '2012-02-28 15:07:59'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTime); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeIso', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '2012-02-28 15:07:59'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeIso); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeShortIso', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '2012-02-28 15:07'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeShortIso); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeIsoAmPm', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '2012-02-28 03:07:59 pm'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeIsoAmPm); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeIsoAM_PM', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '2012-02-28 03:07:59 PM'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeIsoAM_PM); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateEuro', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '28/02/2012'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateEuro); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateEuroShort', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '28/2/12'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateEuroShort); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeEuro', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '28/02/2012 15:07:59'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeEuro); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeShortEuro', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '28/02/2012 15:07'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeShortEuro); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeEuroAmPm', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '28/02/2012 03:07:59 pm'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeEuroAmPm); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeEuroAM_PM', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '28/02/2012 03:07:59 PM'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeEuroAM_PM); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeEuroShort', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '28/2/12 15:7:59'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeEuroShort); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeEuroShortAmPm', async () => { + const input = '2012-02-28 15:07:59'; + const expectedDate = '28/2/12 3:7:59 pm'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeEuroShortAmPm); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateUs', async () => { + const input = new Date('2012-02-28 15:07:59'); + const expectedDate = '02/28/2012'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateUs); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateUsShort', async () => { + const input = new Date('2012-02-28 15:07:59'); + const expectedDate = '2/28/12'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateUsShort); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeUs', async () => { + const input = new Date('2012-02-28 15:07:59'); + const expectedDate = '02/28/2012 15:07:59'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeUs); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeShortUs', async () => { + const input = new Date('2012-02-28 15:07:59'); + const expectedDate = '02/28/2012 15:07'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeShortUs); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeUsAmPm', async () => { + const input = new Date('2012-02-28 15:07:59'); + const expectedDate = '02/28/2012 03:07:59 pm'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeUsAmPm); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeUsAM_PM', async () => { + const input = new Date('2012-02-28 15:07:59'); + const expectedDate = '02/28/2012 03:07:59 PM'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeUsAM_PM); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeUsShort', async () => { + const input = new Date('2012-02-28 15:07:59'); + const expectedDate = '2/28/12 15:7:59'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeUsShort); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.dateTimeUsShortAmPm', async () => { + const input = new Date('2012-02-28 15:07:59'); + const expectedDate = '2/28/12 3:7:59 pm'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateTimeUsShortAmPm); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + xit('should return a date time format when using FieldType.dateUtc', async () => { + const input = moment('2013-05-23T17:55:00.325').utcOffset(420); // timezone that is +7 UTC hours + const expectedDate = '2013-05-24T04:55:00.325+07:00'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.dateUtc); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + + it('should return a date time format when using FieldType.date', async () => { + const input = new Date(Date.UTC(2012, 1, 28, 23, 1, 52, 103)); + const expectedDate = '2012-02-28'; + + service.init(gridStub, dataViewStub); + await service.exportToExcel(mockExportExcelOptions); + const output = service.useCellFormatByFieldType(input, FieldType.date); + + expect(output).toEqual({ metadata: { style: 5 }, value: expectedDate }); + }); + }); + }); + + describe('without I18N Service', () => { + beforeEach(() => { + translateService = null; + service = new ExcelExportService(pubSubService, translateService); + }); + + it('should throw an error if "enableTranslate" is set but the I18N Service is null', () => { + const gridOptionsMock = { enableTranslate: true, enableGridMenu: true, gridMenu: { hideForceFitButton: false, hideSyncResizeButton: true, columnTitleKey: 'TITLE' } } as GridOption; + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + + expect(() => service.init(gridStub, dataViewStub)).toThrowError('[Aurelia-Slickgrid] requires "I18N" to be installed and configured when the grid option "enableTranslate" is enabled.'); + }); + }); +}); diff --git a/packages/excel-export/src/excelExport.service.ts b/packages/excel-export/src/excelExport.service.ts new file mode 100644 index 000000000..7b4e53912 --- /dev/null +++ b/packages/excel-export/src/excelExport.service.ts @@ -0,0 +1,484 @@ +import * as ExcelBuilder from 'excel-builder-webpacker'; +import * as moment_ from 'moment-mini'; +const moment = moment_; // patch to fix rollup "moment has no default export" issue, document here https://github.com/rollup/rollup/issues/670 + +import { + // utility functions + addWhiteSpaces, + deepCopy, + exportWithFormatterWhenDefined, + mapMomentDateFormatWithFieldType, + sanitizeHtmlToText, + titleCase, + + // interfaces + Column, + Constants, + FileType, + FieldType, + GridOption, + KeyTitlePair, + Locale, + PubSubService, + TranslaterService, +} from '@slickgrid-universal/common'; + +import { + ExcelCellFormat, + ExcelExportOption, + ExcelMetadata, + ExcelStylesheet, + ExcelWorkbook, + ExcelWorksheet, +} from './interfaces/index'; + +export class ExcelExportService { + private _fileFormat = FileType.xlsx; + private _dataView: any; + private _grid: any; + private _locales: Locale; + private _columnHeaders: KeyTitlePair[]; + private _groupedHeaders: KeyTitlePair[]; + private _hasGroupedItems = false; + private _excelExportOptions: ExcelExportOption; + private _sheet: ExcelWorksheet; + private _stylesheet: ExcelStylesheet; + private _stylesheetFormats: any; + private _workbook: ExcelWorkbook; + + constructor(private pubSubService: PubSubService, private translaterService: TranslaterService) { } + + private get datasetIdName(): string { + return this._gridOptions && this._gridOptions.datasetIdPropertyName || 'id'; + } + + /** Getter for the Grid Options pulled through the Grid Object */ + private get _gridOptions(): GridOption { + return (this._grid && this._grid.getOptions) ? this._grid.getOptions() : {}; + } + + /** + * Initialize the Export Service + * @param grid + * @param gridOptions + * @param dataView + */ + init(grid: any, dataView: any): void { + this._grid = grid; + this._dataView = dataView; + + // get locales provided by user in main file or else use default English locales via the Constants + this._locales = this._gridOptions && this._gridOptions.locales || Constants.locales; + + if (this._gridOptions.enableTranslate && (!this.translaterService || !this.translaterService.translate)) { + throw new Error('[Aurelia-Slickgrid] requires "I18N" to be installed and configured when the grid option "enableTranslate" is enabled.'); + } + } + + /** + * Function to export the Grid result to an Excel CSV format using javascript for it to produce the CSV file. + * This is a WYSIWYG export to file output (What You See is What You Get) + * + * NOTES: The column position needs to match perfectly the JSON Object position because of the way we are pulling the data, + * which means that if any column(s) got moved in the UI, it has to be reflected in the JSON array output as well + * + * Example: exportToExcel({ format: FileType.csv, delimiter: DelimiterType.comma }) + */ + exportToExcel(options: ExcelExportOption): Promise { + if (!this._grid || !this._dataView) { + throw new Error('[Aurelia-Slickgrid] it seems that the SlickGrid & DataView objects are not initialized did you forget to enable the grid option flag "enableExcelExport"?'); + } + + return new Promise((resolve, reject) => { + this.pubSubService.publish(`onBeforeExportToExcel`, true); + this._excelExportOptions = deepCopy({ ...this._gridOptions.excelExportOptions, options }); + this._fileFormat = this._excelExportOptions.format || FileType.xlsx; + + // prepare the Excel Workbook & Sheet + // we can use ExcelBuilder constructor with WebPack but we need to use function calls with RequireJS/SystemJS + const worksheetOptions = { name: this._excelExportOptions.sheetName || 'Sheet1' }; + this._workbook = ExcelBuilder.Workbook ? new ExcelBuilder.Workbook() : ExcelBuilder.createWorkbook(); + this._sheet = ExcelBuilder.Worksheet ? new ExcelBuilder.Worksheet(worksheetOptions) : this._workbook.createWorksheet(worksheetOptions); + + // add any Excel Format/Stylesheet to current Workbook + this._stylesheet = this._workbook.getStyleSheet(); + const boldFormatter = this._stylesheet.createFormat({ font: { bold: true } }); + const stringFormatter = this._stylesheet.createFormat({ format: '@' }); + const numberFormatter = this._stylesheet.createFormat({ format: '0' }); + const usdFormatter = this._stylesheet.createFormat({ format: '$#,##0.00' }); + this._stylesheetFormats = { + boldFormatter, + dollarFormatter: usdFormatter, + numberFormatter, + stringFormatter, + }; + + // get the CSV output from the grid data + const dataOutput = this.getDataOutput(); + + // trigger a download file + // wrap it into a setTimeout so that the EventAggregator has enough time to start a pre-process like showing a spinner + setTimeout(async () => { + try { + if (this._gridOptions && this._gridOptions.excelExportOptions && this._gridOptions.excelExportOptions.customExcelHeader) { + this._gridOptions.excelExportOptions.customExcelHeader(this._workbook, this._sheet); + } + + const columns = this._grid && this._grid.getColumns && this._grid.getColumns() || []; + this._sheet.setColumns(this.getColumnStyles(columns)); + + const currentSheetData = this._sheet.data; + let finalOutput = currentSheetData; + if (Array.isArray(currentSheetData) && Array.isArray(dataOutput)) { + finalOutput = this._sheet.data.concat(dataOutput); + } + + this._sheet.setData(finalOutput); + this._workbook.addWorksheet(this._sheet); + + // using ExcelBuilder.Builder.createFile with WebPack but ExcelBuilder.createFile with RequireJS/SystemJS + const createFileFn = ExcelBuilder.Builder && ExcelBuilder.Builder.createFile ? ExcelBuilder.Builder.createFile : ExcelBuilder.createFile; + const excelBlob = await createFileFn(this._workbook, { type: 'blob' }); + const downloadOptions = { + filename: `${this._excelExportOptions.filename}.${this._fileFormat}`, + format: this._fileFormat + }; + + // start downloading but add the Blob property only on the start download not on the event itself + this.startDownloadFile({ ...downloadOptions, blob: excelBlob, data: this._sheet.data }); + this.pubSubService.publish(`onAfterExportToExcel`, downloadOptions); + resolve(true); + } catch (error) { + reject(error); + } + }); + }); + } + + /** + * Triggers download file with file format. + * IE(6-10) are not supported + * All other browsers will use plain javascript on client side to produce a file download. + * @param options + */ + startDownloadFile(options: { filename: string, blob: Blob, data: any[] }) { + // IE(6-10) don't support javascript download and our service doesn't support either so throw an error, we have to make a round trip to the Web Server for exporting + if (navigator.appName === 'Microsoft Internet Explorer') { + throw new Error('Microsoft Internet Explorer 6 to 10 do not support javascript export to Excel. Please upgrade your browser.'); + } + + // when using IE/Edge, then use different download call + if (typeof navigator.msSaveOrOpenBlob === 'function') { + navigator.msSaveOrOpenBlob(options.blob, options.filename); + } else { + // this trick will generate a temp tag + // the code will then trigger a hidden click for it to start downloading + const link = document && document.createElement('a'); + const url = URL.createObjectURL(options.blob); + + if (link && document) { + link.textContent = 'download'; + link.href = url; + link.setAttribute('download', options.filename); + + // set the visibility to hidden so there is no effect on your web-layout + link.style.visibility = 'hidden'; + + // this part will append the anchor tag, trigger a click (for download to start) and finally remove the tag once completed + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } + } + + /** use different Excel Stylesheet Format as per the Field Type */ + useCellFormatByFieldType(data: string | Date | moment_.Moment, fieldType: FieldType): ExcelCellFormat | string { + let outputData: ExcelCellFormat | string | Date | moment_.Moment = data; + switch (fieldType) { + case FieldType.dateTime: + case FieldType.dateTimeIso: + case FieldType.dateTimeShortIso: + case FieldType.dateTimeIsoAmPm: + case FieldType.dateTimeIsoAM_PM: + case FieldType.dateEuro: + case FieldType.dateEuroShort: + case FieldType.dateTimeEuro: + case FieldType.dateTimeShortEuro: + case FieldType.dateTimeEuroAmPm: + case FieldType.dateTimeEuroAM_PM: + case FieldType.dateTimeEuroShort: + case FieldType.dateTimeEuroShortAmPm: + case FieldType.dateUs: + case FieldType.dateUsShort: + case FieldType.dateTimeUs: + case FieldType.dateTimeShortUs: + case FieldType.dateTimeUsAmPm: + case FieldType.dateTimeUsAM_PM: + case FieldType.dateTimeUsShort: + case FieldType.dateTimeUsShortAmPm: + case FieldType.dateUtc: + case FieldType.date: + case FieldType.dateIso: + outputData = data; + if (data) { + const defaultDateFormat = mapMomentDateFormatWithFieldType(fieldType); + const isDateValid = moment(data as string, defaultDateFormat, false).isValid(); + const outputDate = (data && isDateValid) ? moment(data as string).format(defaultDateFormat) : data; + const dateFormatter = this._stylesheet.createFormat({ format: defaultDateFormat }); + outputData = { value: outputDate, metadata: { style: dateFormatter.id } }; + } + break; + case FieldType.number: + const val = isNaN(+data) ? null : data; + outputData = { value: val, metadata: { style: this._stylesheetFormats.numberFormatter.id } }; + break; + default: + outputData = data; + } + return outputData as string; + } + + // ----------------------- + // Private functions + // ----------------------- + + private getDataOutput(): Array { + const columns = this._grid && this._grid.getColumns && this._grid.getColumns() || []; + + // data variable which will hold all the fields data of a row + const outputData: Array = []; + const columnHeaderStyle = this._gridOptions && this._gridOptions.excelExportOptions && this._gridOptions.excelExportOptions.columnHeaderStyle; + let columnHeaderStyleId = this._stylesheetFormats.boldFormatter.id; + if (columnHeaderStyle) { + columnHeaderStyleId = this._stylesheet.createFormat(columnHeaderStyle).id; + } + + // get all column headers (it might include a "Group by" title at A1 cell) + // also style the headers, defaults to Bold but user could pass his own style + outputData.push(this.getColumnHeaderData(columns, { style: columnHeaderStyleId })); + + // Populate the rest of the Grid Data + this.pushAllGridRowDataToArray(outputData, columns); + + return outputData; + } + + /** Get each column style including a style for the width of each column */ + private getColumnStyles(columns: Column[]): any[] { + const grouping = this._dataView && this._dataView.getGrouping && this._dataView.getGrouping(); + const columnStyles = []; + if (grouping) { + columnStyles.push({ + bestFit: true, + columnStyles: (this._gridOptions && this._gridOptions.excelExportOptions && this._gridOptions.excelExportOptions.customColumnWidth) || 10 + }); + } + + columns.forEach((columnDef: Column) => { + const skippedField = columnDef.excludeFromExport || false; + // if column width is 0, then we consider that field as a hidden field and should not be part of the export + if ((columnDef.width === undefined || columnDef.width > 0) && !skippedField) { + columnStyles.push({ + bestFit: true, + width: columnDef.exportColumnWidth || (this._gridOptions && this._gridOptions.excelExportOptions && this._gridOptions.excelExportOptions.customColumnWidth) || 10 + }); + } + }); + return columnStyles; + } + + /** Get all column headers and format them in Bold */ + private getColumnHeaderData(columns: Column[], metadata: ExcelMetadata): Array { + let outputHeaderTitles: Array = []; + + this._columnHeaders = this.getColumnHeaders(columns) || []; + if (this._columnHeaders && Array.isArray(this._columnHeaders) && this._columnHeaders.length > 0) { + // add the header row + add a new line at the end of the row + outputHeaderTitles = this._columnHeaders.map((header) => ({ value: header.title, metadata })); + } + + // do we have a Group by title? + const groupTitle = this.getGroupColumnTitle(); + if (groupTitle) { + outputHeaderTitles.unshift({ value: groupTitle, metadata }); + } + + return outputHeaderTitles; + } + + private getGroupColumnTitle(): string | null { + // Group By text, it could be set in the export options or from translation or if nothing is found then use the English constant text + let groupByColumnHeader = this._excelExportOptions.groupingColumnHeaderTitle; + if (!groupByColumnHeader && this._gridOptions.enableTranslate && this.translaterService && this.translaterService.translate && this.translaterService.getCurrentLocale && this.translaterService.getCurrentLocale()) { + groupByColumnHeader = this.translaterService.translate('GROUP_BY'); + } else if (!groupByColumnHeader) { + groupByColumnHeader = this._locales && this._locales.TEXT_GROUP_BY; + } + + // get grouped column titles and if found, we will add a "Group by" column at the first column index + // if it's a CSV format, we'll escape the text in double quotes + const grouping = this._dataView && this._dataView.getGrouping && this._dataView.getGrouping(); + if (grouping && Array.isArray(grouping) && grouping.length > 0) { + this._hasGroupedItems = true; + return groupByColumnHeader; + } else { + this._hasGroupedItems = false; + } + return null; + } + + /** + * Get all header titles and their keys, translate the title when required. + * @param columns of the grid + */ + private getColumnHeaders(columns: Column[]): KeyTitlePair[] | null { + if (!columns || !Array.isArray(columns) || columns.length === 0) { + return null; + } + const columnHeaders: KeyTitlePair[] = []; + + // Populate the Column Header, pull the name defined + columns.forEach((columnDef) => { + let headerTitle = ''; + if ((columnDef.nameKey || columnDef.nameKey) && this._gridOptions.enableTranslate && this.translaterService && this.translaterService.translate && this.translaterService.getCurrentLocale && this.translaterService.getCurrentLocale()) { + headerTitle = this.translaterService.translate((columnDef.nameKey || columnDef.nameKey)); + } else { + headerTitle = columnDef.name || titleCase(columnDef.field); + } + const skippedField = columnDef.excludeFromExport || false; + + // if column width is 0, then we consider that field as a hidden field and should not be part of the export + if ((columnDef.width === undefined || columnDef.width > 0) && !skippedField) { + columnHeaders.push({ + key: (columnDef.field || columnDef.id) + '', + title: headerTitle + }); + } + }); + + return columnHeaders; + } + + /** + * Get all the grid row data and return that as an output string + */ + private pushAllGridRowDataToArray(originalDaraArray: Array, columns: Column[]): Array { + const lineCount = this._dataView && this._dataView.getLength && this._dataView.getLength(); + + // loop through all the grid rows of data + for (let rowNumber = 0; rowNumber < lineCount; rowNumber++) { + const itemObj = this._dataView.getItem(rowNumber); + if (itemObj != null) { + // Normal row (not grouped by anything) would have an ID which was predefined in the Grid Columns definition + if (itemObj[this.datasetIdName] != null) { + // get regular row item data + originalDaraArray.push(this.readRegularRowData(columns, rowNumber, itemObj)); + } else if (this._hasGroupedItems && itemObj.__groupTotals === undefined) { + // get the group row + originalDaraArray.push([this.readGroupedRowTitle(itemObj)]); + } else if (itemObj.__groupTotals) { + // else if the row is a Group By and we have agreggators, then a property of '__groupTotals' would exist under that object + originalDaraArray.push(this.readGroupedTotalRows(columns, itemObj)); + } + } + } + return originalDaraArray; + } + + /** + * Get the data of a regular row (a row without grouping) + * @param row + * @param itemObj + */ + private readRegularRowData(columns: Column[], row: number, itemObj: any): string[] { + let idx = 0; + const rowOutputStrings: string[] = []; + + for (let col = 0, ln = columns.length; col < ln; col++) { + const columnDef = columns[col]; + const fieldType = columnDef.outputType || columnDef.type || FieldType.string; + + // skip excluded column + if (columnDef.excludeFromExport) { + continue; + } + + // if we are grouping and are on 1st column index, we need to skip this column since it will be used later by the grouping text:: Group by [columnX] + if (this._hasGroupedItems && idx === 0) { + rowOutputStrings.push(''); + } + + // get the output by analyzing if we'll pull the value from the cell or from a formatter + let itemData: ExcelCellFormat | string = exportWithFormatterWhenDefined(row, col, itemObj, columnDef, this._grid, this._excelExportOptions); + + // does the user want to sanitize the output data (remove HTML tags)? + if (columnDef.sanitizeDataExport || this._excelExportOptions.sanitizeDataExport) { + itemData = sanitizeHtmlToText(itemData as string); + } + + // use different Excel Stylesheet Format as per the Field Type + if (!columnDef.exportWithFormatter) { + itemData = this.useCellFormatByFieldType(itemData as string, fieldType); + } + + rowOutputStrings.push(itemData as string); + idx++; + } + + return rowOutputStrings; + } + + /** + * Get the grouped title(s) and its group title formatter, for example if we grouped by salesRep, the returned result would be:: 'Sales Rep: John Dow (2 items)' + * @param itemObj + */ + private readGroupedRowTitle(itemObj: any): string { + const groupName = sanitizeHtmlToText(itemObj.title); + + if (this._excelExportOptions && this._excelExportOptions.addGroupIndentation) { + const collapsedSymbol = this._excelExportOptions && this._excelExportOptions.groupCollapsedSymbol || '\u25B9'; + const expandedSymbol = this._excelExportOptions && this._excelExportOptions.groupExpandedSymbol || '\u25BF'; + const chevron = itemObj.collapsed ? collapsedSymbol : expandedSymbol; + return chevron + ' ' + addWhiteSpaces(5 * itemObj.level) + groupName; + } + return groupName; + } + + /** + * Get the grouped totals (below the regular rows), these are set by Slick Aggregators. + * For example if we grouped by "salesRep" and we have a Sum Aggregator on "sales", then the returned output would be:: ["Sum 123$"] + * @param itemObj + */ + private readGroupedTotalRows(columns: Column[], itemObj: any): string[] { + const groupingAggregatorRowText = this._excelExportOptions.groupingAggregatorRowText || ''; + const outputStrings = [groupingAggregatorRowText]; + + columns.forEach((columnDef) => { + let itemData = ''; + + const skippedField = columnDef.excludeFromExport || false; + + // if there's a exportCustomGroupTotalsFormatter or groupTotalsFormatter, we will re-run it to get the exact same output as what is shown in UI + if (columnDef.exportCustomGroupTotalsFormatter) { + itemData = columnDef.exportCustomGroupTotalsFormatter(itemObj, columnDef); + } else { + if (columnDef.groupTotalsFormatter) { + itemData = columnDef.groupTotalsFormatter(itemObj, columnDef); + } + } + + // does the user want to sanitize the output data (remove HTML tags)? + if (columnDef.sanitizeDataExport || this._excelExportOptions.sanitizeDataExport) { + itemData = sanitizeHtmlToText(itemData); + } + + // add the column (unless user wants to skip it) + if ((columnDef.width === undefined || columnDef.width > 0) && !skippedField) { + outputStrings.push(itemData); + } + }); + + return outputStrings; + } +} diff --git a/packages/excel-export/src/index.ts b/packages/excel-export/src/index.ts new file mode 100644 index 000000000..39a1c78d4 --- /dev/null +++ b/packages/excel-export/src/index.ts @@ -0,0 +1 @@ +export { ExcelExportService } from './excelExport.service'; diff --git a/packages/excel-export/src/interfaces/excelCellFormat.interface.ts b/packages/excel-export/src/interfaces/excelCellFormat.interface.ts new file mode 100644 index 000000000..fac813052 --- /dev/null +++ b/packages/excel-export/src/interfaces/excelCellFormat.interface.ts @@ -0,0 +1,6 @@ +import { ExcelMetadata } from './excelMetadata.interface'; + +export interface ExcelCellFormat { + value: any; + metadata: ExcelMetadata; +} diff --git a/packages/excel-export/src/interfaces/excelCopyBufferOption.interface.ts b/packages/excel-export/src/interfaces/excelCopyBufferOption.interface.ts new file mode 100644 index 000000000..6551721ed --- /dev/null +++ b/packages/excel-export/src/interfaces/excelCopyBufferOption.interface.ts @@ -0,0 +1,55 @@ +import { Column, SelectedRange } from '@slickgrid-universal/common'; + +export interface ExcelCopyBufferOption { + /** defaults to "copied", sets the css className used for copied cells. */ + copiedCellStyle?: string; + + /** defaults to "copy-manager", sets the layer key for setting css values of copied cells. */ + copiedCellStyleLayerKey?: string; + + /** option to specify a custom column value extractor function */ + dataItemColumnValueExtractor?: (item: any, columnDef: Column) => any; + + /** option to specify a custom column value setter function */ + dataItemColumnValueSetter?: (item: any, columnDef: Column, value: any) => any; + + /** option to specify a custom handler for paste actions */ + clipboardCommandHandler?: (editCommand: any) => void; + + /** set to true and the plugin will take the name property from each column (which is usually what appears in your header) and put that as the first row of the text that's copied to the clipboard */ + includeHeaderWhenCopying?: boolean; + + /** option to specify a custom DOM element which to will be added the hidden textbox. It's useful if the grid is inside a modal dialog. */ + bodyElement?: HTMLElement; + + /** optional handler to run when copy action initializes */ + onCopyInit?: any; + + /** optional handler to run when copy action is complete */ + onCopySuccess?: any; + + /** function to add rows to table if paste overflows bottom of table, if this function is not provided new rows will be ignored. */ + newRowCreator?: (count: number) => void; + + /** suppresses paste */ + readOnlyMode?: boolean; + + /** option to specify a custom column header value extractor function */ + headerColumnValueExtractor?: (columnDef: Column) => any; + + // -- + // Events + // ------------ + + /** Fired after extension (plugin) is registered by SlickGrid */ + onExtensionRegistered?: (plugin: any) => void; + + /** Fired when a copy cell is triggered */ + onCopyCells?: (e: Event, args: { ranges: SelectedRange[] }) => void; + + /** Fired when the command to copy the cells is cancelled */ + onCopyCancelled?: (e: Event, args: { ranges: SelectedRange[] }) => void; + + /** Fired when the user paste cells to the grid */ + onPasteCells?: (e: Event, args: { ranges: SelectedRange[] }) => void; +} diff --git a/packages/excel-export/src/interfaces/excelExportOption.interface.ts b/packages/excel-export/src/interfaces/excelExportOption.interface.ts new file mode 100644 index 000000000..b532fe6d3 --- /dev/null +++ b/packages/excel-export/src/interfaces/excelExportOption.interface.ts @@ -0,0 +1,44 @@ +import { FileType } from '@slickgrid-universal/common'; +import { ExcelWorksheet } from './excelWorksheet.interface'; +import { ExcelWorkbook } from './excelWorkbook.interface'; + +export interface ExcelExportOption { + /** Defaults to true, when grid is using Grouping, it will show indentation of the text with collapsed/expanded symbol as well */ + addGroupIndentation?: boolean; + + /** If defined apply the style to header columns. Else use the bold style */ + columnHeaderStyle?: any; + + /** If set then this will be used as column width for all columns */ + customColumnWidth?: number; + + /** Defaults to false, which leads to all Formatters of the grid being evaluated on export. You can also override a column by changing the propery on the column itself */ + exportWithFormatter?: boolean; + + /** filename (without extension) */ + filename?: string; + + /** file type format, .xls/.xlsx (this will provide the extension) */ + format?: FileType; + + /** The column header title (at A0 in Excel) of the Group by. If nothing is provided it will use "Group By" (which is a translated value of GROUP_BY i18n) */ + groupingColumnHeaderTitle?: string; + + /** The default text to display in 1st column of the File Export, which will identify that the current row is a Grouping Aggregator */ + groupingAggregatorRowText?: string; + + /** Symbol use to show that the group title is collapsed (you can use unicode like '\u25B9' or '\u25B7') */ + groupCollapsedSymbol?: string; + + /** Symbol use to show that the group title is expanded (you can use unicode like '\u25BF' or '\u25BD') */ + groupExpandedSymbol?: string; + + /** Defaults to false, which leads to Sanitizing all data (striping out any HTML tags) when being evaluated on export. */ + sanitizeDataExport?: boolean; + + /** Defaults to "Sheet1", Excel Sheet Name */ + sheetName?: string; + + /** Add a Custom Excel Header on first row of the Excel Sheet */ + customExcelHeader?: (workbook: ExcelWorkbook, sheet: ExcelWorksheet) => void; +} diff --git a/packages/excel-export/src/interfaces/excelMetadata.interface.ts b/packages/excel-export/src/interfaces/excelMetadata.interface.ts new file mode 100644 index 000000000..1b79549c7 --- /dev/null +++ b/packages/excel-export/src/interfaces/excelMetadata.interface.ts @@ -0,0 +1,9 @@ +export interface ExcelMetadata { + alignment?: any; + border?: any; + font?: any; + format?: any; + style?: any; + type?: any; + protection?: any; +} diff --git a/packages/excel-export/src/interfaces/excelStylesheet.interface.ts b/packages/excel-export/src/interfaces/excelStylesheet.interface.ts new file mode 100644 index 000000000..33df51f3d --- /dev/null +++ b/packages/excel-export/src/interfaces/excelStylesheet.interface.ts @@ -0,0 +1,32 @@ +export interface ExcelStylesheet { + createFill: (instructions: any) => any; + createSimpleFormatter: (type: any) => any; + createFormat: (instructions: any) => any; + createNumberFormatter: (instructions: any) => any; + createDifferentialStyle: (instructions: any) => any; + createTableStyle: (instructions: any) => any; + createBorderFormatter: (border: any) => any; + exportCellFormatElement: (doc: any, instructions: any) => any; + exportAlignment: (doc: any, alignmentData: any) => any; + createFontStyle: (instructions: any) => any; + exportBorder: (doc: any, data: any[]) => any; + exportBorders: (doc: any) => any; + exportColor: (doc: any, color: any) => any; + exportFont: (doc: any, fd: any) => any; + exportFonts: (doc: any) => any; + exportFill: (doc: any, fd: any) => any; + exportFills: (doc: any) => any; + exportGradientFill: (doc: any, data: any[]) => any; + exportPatternFill: (doc: any, data: any[]) => any; + exportNumberFormatter: (doc: any, fd: any) => any; + exportNumberFormatters: (doc: any) => any; + exportCellStyles: (doc: any) => any; + exportDifferentialStyles: (doc: any) => any; + exportMasterCellFormats: (doc: any) => any; + exportMasterCellStyles: (doc: any) => any; + exportTableStyle: (doc: any, style: any) => any; + exportTableStyles: (doc: any) => any; + exportProtection: (doc: any, protectionData: any) => any; + exportDFX: (doc: any, style: any) => any; + toXML: () => any; +} diff --git a/packages/excel-export/src/interfaces/excelWorkbook.interface.ts b/packages/excel-export/src/interfaces/excelWorkbook.interface.ts new file mode 100644 index 000000000..58ad6bd53 --- /dev/null +++ b/packages/excel-export/src/interfaces/excelWorkbook.interface.ts @@ -0,0 +1,17 @@ +import { ExcelWorksheet } from './excelWorksheet.interface'; +import { ExcelStylesheet } from './excelStylesheet.interface'; + +export interface ExcelWorkbook { + addDrawings: (drawings: any) => any; + addTable: (table: any) => any; + addWorksheet: (worksheet: any) => any; + addMedia: (type: any, fileName: any, fileData: any, contentType: any) => any; + createContentTypes: () => any; + createWorksheet: (config: any) => ExcelWorksheet; + createWorkbookRelationship: () => any; + generateFiles: () => any; + getStyleSheet: () => ExcelStylesheet; + setPrintTitleTop: (inSheet: any, inRowCount: any) => any; + setPrintTitleLeft: (inSheet: any, inRowCount: any) => any; + toXML: () => any; +} diff --git a/packages/excel-export/src/interfaces/excelWorksheet.interface.ts b/packages/excel-export/src/interfaces/excelWorksheet.interface.ts new file mode 100644 index 000000000..c7782e1d2 --- /dev/null +++ b/packages/excel-export/src/interfaces/excelWorksheet.interface.ts @@ -0,0 +1,36 @@ +export interface ExcelWorksheet { + relations: any; + columnFormats: any[]; + data: any[]; + mergedCells: any[]; + columns: any[]; + sheetProtection: boolean; + hyperlinks: any[]; + sheetView: any; + showZeros: boolean; + + initialize: (config: any) => any; + compilePageDetailPackage: (data: any) => any; + compilePageDetailPiece: (data: any) => any; + exportFooter: (doc: any) => any; + exportHeader: (doc: any) => any; + collectSharedStrings: () => any[]; + exportColumns: (doc: any) => any; + exportPageSettings: (doc: any, worksheet: any) => any; + exportData: () => any; + addDrawings: (drawings: any) => void; + addTable: (table: any) => void; + freezePane: (column: number, row: number, cell: string) => void; + importData: (data: any) => void; + mergeCells: (cell1: string, cell2: string) => void; + setColumns: (columns: any[]) => void; + setColumnFormats: (columnFormats: any[]) => void; + setData: (data: any[]) => void; + setFooter: (footers: any) => void; + setHeader: (headers: any) => void; + setPageOrientation: (orientation: any) => void; + setPageMargin: (input: any) => void; + setSharedStringCollection: (collection: string[]) => void; + setRowInstructions: (row: number, instructions: any) => void; + toXML: () => any; +} diff --git a/packages/excel-export/src/interfaces/index.ts b/packages/excel-export/src/interfaces/index.ts new file mode 100644 index 000000000..964e4f4c2 --- /dev/null +++ b/packages/excel-export/src/interfaces/index.ts @@ -0,0 +1,7 @@ +export * from './excelCellFormat.interface'; +export * from './excelCopyBufferOption.interface'; +export * from './excelExportOption.interface'; +export * from './excelMetadata.interface'; +export * from './excelStylesheet.interface'; +export * from './excelWorkbook.interface'; +export * from './excelWorksheet.interface'; diff --git a/packages/excel-export/src/typings/excel-builder-webpacker/index.d.ts b/packages/excel-export/src/typings/excel-builder-webpacker/index.d.ts new file mode 100644 index 000000000..df1162e32 --- /dev/null +++ b/packages/excel-export/src/typings/excel-builder-webpacker/index.d.ts @@ -0,0 +1 @@ +declare module 'excel-builder-webpacker'; diff --git a/packages/excel-export/src/typings/global.d.ts b/packages/excel-export/src/typings/global.d.ts new file mode 100644 index 000000000..9e6c1dbd9 --- /dev/null +++ b/packages/excel-export/src/typings/global.d.ts @@ -0,0 +1 @@ +import 'jest-extended'; diff --git a/packages/excel-export/src/typings/typings.d.ts b/packages/excel-export/src/typings/typings.d.ts new file mode 100644 index 000000000..65ed24ef9 --- /dev/null +++ b/packages/excel-export/src/typings/typings.d.ts @@ -0,0 +1,12 @@ +/* SystemJS module definition */ +declare const module: NodeModule; +interface NodeModule { + id: string; +} +interface jquery { + tooltip(options?: any): any; + tipsy(options?: any): any; +} +interface window { + Slicker: any; +} diff --git a/packages/excel-export/tsconfig.build.json b/packages/excel-export/tsconfig.build.json new file mode 100644 index 000000000..3beb8c48a --- /dev/null +++ b/packages/excel-export/tsconfig.build.json @@ -0,0 +1,46 @@ +{ + "compilerOptions": { + "module": "es2020", + "moduleResolution": "node", + "target": "es2015", + "lib": [ + "es2020", + "dom" + ], + "typeRoots": [ + "../typings", + "../../node_modules/@types" + ], + "outDir": "dist/amd", + "noImplicitAny": true, + "suppressImplicitAnyIndexErrors": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "skipLibCheck": true, + "strictNullChecks": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "noEmitHelpers": false, + "stripInternal": true, + "sourceMap": true, + "baseUrl": "./", + "paths": { + "jszip": [ + "../node_modules/jszip/dist/jszip.min.js" + ] + } + }, + "exclude": [ + ".vscode", + "src/examples", + "src/resources", + "test", + "**/*.spec.ts" + ], + "include": [ + "../typings", + "**/*" + ] +} diff --git a/packages/excel-export/tsconfig.json b/packages/excel-export/tsconfig.json new file mode 100644 index 000000000..56398182e --- /dev/null +++ b/packages/excel-export/tsconfig.json @@ -0,0 +1,47 @@ +{ + "extends": "../tsconfig-build.json", + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "rootDir": "src", + "declarationDir": "dist/es2015", + "outDir": "dist/es2015", + "target": "es2015", + "module": "esnext", + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "noImplicitReturns": true, + "lib": [ + "es2020", + "dom" + ], + "types": [ + "moment", + "node" + ], + "typeRoots": [ + "node_modules/@types", + "src/typings" + ], + "paths": { + "jszip": [ + "node_modules/jszip/dist/jszip.min.js" + ] + } + }, + "exclude": [ + "cypress", + "dist", + "node_modules", + "**/*.spec.ts" + ], + "filesGlob": [ + "./src/**/*.ts", + "./test/**/*.ts", + "./custom_typings/**/*.d.ts" + ], + "include": [ + "src/**/*.ts", + "src/typings/**/*.ts" + ] +} diff --git a/packages/export/src/export.service.ts b/packages/export/src/export.service.ts index c4481fbd5..ca217730c 100644 --- a/packages/export/src/export.service.ts +++ b/packages/export/src/export.service.ts @@ -20,8 +20,6 @@ import { TranslaterService, } from '@slickgrid-universal/common'; -const DEFAULT_AURELIA_EVENT_PREFIX = 'asg'; - export class ExportService { private _delimiter = ','; private _exportQuoteWrapper = ''; diff --git a/packages/vanilla-bundle/src/services/excelExport.service.ts b/packages/vanilla-bundle/src/services/excelExport.service.ts index 0646cb848..198409efc 100644 --- a/packages/vanilla-bundle/src/services/excelExport.service.ts +++ b/packages/vanilla-bundle/src/services/excelExport.service.ts @@ -1,11 +1,19 @@ -import { ExcelExportService, ExcelExportOption } from '@slickgrid-universal/common'; +import { ExcelExportOption } from '@slickgrid-universal/common'; +import { TranslateService } from './translate.service'; +import { EventPubSubService } from './eventPubSub.service'; -export class ExcelExportServicer implements ExcelExportService { +export class ExcelExportService { + constructor(eventPubSubService: EventPubSubService, translateService: TranslateService) { + // super(eventPubSubService, translateService); + } + init(grid: any, dataView: any): void { + // super.init(grid, dataView); } exportToExcel(options: ExcelExportOption): Promise { return new Promise((resolve) => resolve(true)); + // return super.exportToExcel(options); } } diff --git a/packages/vanilla-bundle/src/vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/vanilla-grid-bundle.ts index 6c6bd29af..ed952f84b 100644 --- a/packages/vanilla-bundle/src/vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/vanilla-grid-bundle.ts @@ -49,7 +49,7 @@ import { } from '@slickgrid-universal/common'; import { ExportService } from './services/export.service'; -import { ExcelExportServicer } from './services/excelExport.service'; +import { ExcelExportService } from './services/excelExport.service'; import { TranslateService } from './services/translate.service'; import { EventPubSubService } from './services/eventPubSub.service'; import { FooterService } from './services/footer.service'; @@ -88,7 +88,7 @@ export class VanillaGridBundle { columnPickerExtension: ColumnPickerExtension; checkboxExtension: CheckboxSelectorExtension; draggableGroupingExtension: DraggableGroupingExtension; - excelExportServicer: ExcelExportServicer; + excelExportService: ExcelExportService; exportService: ExportService; gridMenuExtension: GridMenuExtension; groupItemMetaProviderExtension: GroupItemMetaProviderExtension; @@ -165,7 +165,7 @@ export class VanillaGridBundle { this.sharedService = new SharedService(); this.translateService = new TranslateService(); this.exportService = new ExportService(this._eventPubSubService, this.translateService); - this.excelExportServicer = new ExcelExportServicer(); + this.excelExportService = new ExcelExportService(this._eventPubSubService, this.translateService); this.collectionService = new CollectionService(this.translateService); this.footerService = new FooterService(this.sharedService, this.translateService); const filterFactory = new FilterFactory(slickgridConfig, this.collectionService, this.translateService); @@ -176,11 +176,11 @@ export class VanillaGridBundle { this.autoTooltipExtension = new AutoTooltipExtension(this.extensionUtility, this.sharedService); this.cellExternalCopyManagerExtension = new CellExternalCopyManagerExtension(this.extensionUtility, this.sharedService); this.cellMenuExtension = new CellMenuExtension(this.extensionUtility, this.sharedService, this.translateService); - this.contextMenuExtension = new ContextMenuExtension(this.excelExportServicer, this.exportService, this.extensionUtility, this.sharedService, this.translateService); + this.contextMenuExtension = new ContextMenuExtension(this.excelExportService, this.exportService, this.extensionUtility, this.sharedService, this.translateService); this.columnPickerExtension = new ColumnPickerExtension(this.extensionUtility, this.sharedService); this.checkboxExtension = new CheckboxSelectorExtension(this.extensionUtility, this.sharedService); this.draggableGroupingExtension = new DraggableGroupingExtension(this.extensionUtility, this.sharedService); - this.gridMenuExtension = new GridMenuExtension(this.excelExportServicer, this.exportService, this.extensionUtility, this.filterService, this.sharedService, this.sortService, this.translateService); + this.gridMenuExtension = new GridMenuExtension(this.excelExportService, this.exportService, this.extensionUtility, this.filterService, this.sharedService, this.sortService, this.translateService); this.groupItemMetaProviderExtension = new GroupItemMetaProviderExtension(this.sharedService); this.headerButtonExtension = new HeaderButtonExtension(this.extensionUtility, this.sharedService); this.headerMenuExtension = new HeaderMenuExtension(this.extensionUtility, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.translateService); @@ -319,6 +319,7 @@ export class VanillaGridBundle { // bind & initialize the grid service this.gridService.init(this.grid, this.dataView); this.gridStateService.init(this.grid, this.dataView); + this.excelExportService.init(this.grid, this.dataView); this.exportService.init(this.grid, this.dataView); // this.paginationService.init(this.grid, this.dataView);