diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts index 614916cf7..1346adb99 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts @@ -220,8 +220,8 @@ export class Example12 { id: 'complexity', name: 'Complexity', field: 'complexity', minWidth: 100, type: FieldType.number, sortable: true, filterable: true, columnGroup: 'Analysis', - formatter: (_row, _cell, value) => this.complexityLevelList[value].label, - exportCustomFormatter: (_row, _cell, value) => this.complexityLevelList[value].label, + formatter: (_row, _cell, value) => this.complexityLevelList[value]?.label, + exportCustomFormatter: (_row, _cell, value) => this.complexityLevelList[value]?.label, filter: { model: Filters.multipleSelect, collection: this.complexityLevelList diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example16.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example16.ts index 2349265c5..69d442139 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example16.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example16.ts @@ -128,16 +128,22 @@ export class Example16 { }, }, { - id: 'cost', name: 'Cost', field: 'cost', + id: 'cost', name: 'Cost (in €)', field: 'cost', width: 90, sortable: true, filterable: true, - exportWithFormatter: false, + exportWithFormatter: true, // filter: { model: Filters.compoundInput }, - // formatter: Formatters.dollar, + // formatter: Formatters.currency, formatter: Formatters.multiple, - // params: { formatters: [Formatters.dollar, (row, cell, value) => `${value || ''}`] }, - params: { formatters: [Formatters.dollar, (row, cell, value) => `${value || ''}`] }, + // params: { formatters: [Formatters.currency, (row, cell, value) => `${value || ''}`] }, + params: { + formatters: [ + Formatters.currency, + (row, cell, value) => `${value || ''}` + ], + currencySuffix: ' €' + }, customTooltip: { useRegularTooltip: true, useRegularTooltipFromFormatterOnly: true, @@ -328,6 +334,10 @@ export class Example16 { textExportOptions: { exportWithFormatter: true }, + formatterOptions: { + // decimalSeparator: ',', + thousandSeparator: ' ' + }, // Custom Tooltip options can be defined in a Column or Grid Options or a mixed of both (first options found wins) registerExternalResources: [new SlickCustomTooltip(), new ExcelExportService(), new TextExportService()], customTooltip: { @@ -393,7 +403,7 @@ export class Example16 { percentComplete: Math.floor(Math.random() * (100 - 5 + 1) + 5), start: new Date(randomYear, randomMonth, randomDay), finish: randomFinish < new Date() ? '' : randomFinish, // make sure the random date is earlier than today - cost: (i % 33 === 0) ? null : Math.round(Math.random() * 10000) / 100, + cost: (i % 33 === 0) ? null : Math.round(Math.random() * 1000000) / 100, effortDriven: (i % 5 === 0), prerequisites: (i % 2 === 0) && i !== 0 && i < 50 ? [i, i - 1] : [], }; diff --git a/package.json b/package.json index 2f6e83823..ab02b3d07 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "cypress:ci": "cypress run --config-file test/cypress.config.ts", "predev": "pnpm run -r build:incremental && pnpm run -r sass:copy", "dev": "run-p dev:watch webpack:watch", - "dev:watch": "lerna watch --file-delimiter=\",\" --glob=\"src/**/*.{ts,scss}\" --ignored=\"src/**/*.spec.ts\" -- cross-env-shell pnpm run -r --filter $LERNA_PACKAGE_NAME dev", + "dev:watch": "lerna watch --no-bail --file-delimiter=\",\" --glob=\"src/**/*.{ts,scss}\" --ignored=\"src/**/*.spec.ts\" -- cross-env-shell pnpm run -r --filter $LERNA_PACKAGE_NAME dev", "webpack:watch": "pnpm -r --parallel run webpack:dev", "preview:publish": "lerna publish from-package --dry-run", "preview:version": "lerna version --dry-run", diff --git a/packages/common/src/formatters/__tests__/formatterUtilities.spec.ts b/packages/common/src/formatters/__tests__/formatterUtilities.spec.ts index 1caad128f..e3d39ec08 100644 --- a/packages/common/src/formatters/__tests__/formatterUtilities.spec.ts +++ b/packages/common/src/formatters/__tests__/formatterUtilities.spec.ts @@ -59,27 +59,23 @@ describe('formatterUtilities', () => { describe('getValueFromParamsOrGridOptions method', () => { it('should return options found in the Grid Option when not found in Column Definition "params" property', () => { const gridOptions = { formatterOptions: { minDecimal: 2 } } as GridOption; - const gridSpy = (gridStub.getOptions as jest.Mock).mockReturnValue(gridOptions); - const output = getValueFromParamsOrFormatterOptions('minDecimal', {} as Column, gridStub, -1); + const output = getValueFromParamsOrFormatterOptions('minDecimal', {} as Column, gridOptions, -1); - expect(gridSpy).toHaveBeenCalled(); expect(output).toBe(2); }); it('should return options found in the Column Definition "params" even if exist in the Grid Option as well', () => { const gridOptions = { formatterOptions: { minDecimal: 2 } } as GridOption; - const gridSpy = (gridStub.getOptions as jest.Mock).mockReturnValue(gridOptions); - const output = getValueFromParamsOrFormatterOptions('minDecimal', { params: { minDecimal: 3 } } as Column, gridStub, -1); + const output = getValueFromParamsOrFormatterOptions('minDecimal', { params: { minDecimal: 3 } } as Column, gridOptions, -1); - expect(gridSpy).toHaveBeenCalled(); expect(output).toBe(3); }); it('should return default value when not found in "params" (columnDef) neither the "formatterOptions" (gridOption)', () => { const defaultValue = 5; - const output = getValueFromParamsOrFormatterOptions('minDecimal', { field: 'column1' } as Column, {} as unknown as SlickGrid, defaultValue); + const output = getValueFromParamsOrFormatterOptions('minDecimal', { field: 'column1' } as Column, {} as unknown as GridOption, defaultValue); expect(output).toBe(defaultValue); }); }); diff --git a/packages/common/src/formatters/formatterUtilities.ts b/packages/common/src/formatters/formatterUtilities.ts index 41846d1a8..aa19cb15e 100644 --- a/packages/common/src/formatters/formatterUtilities.ts +++ b/packages/common/src/formatters/formatterUtilities.ts @@ -59,17 +59,18 @@ export function retrieveFormatterOptions(columnDef: Column, grid: SlickGrid, num default: break; } - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, defaultMinDecimal); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, defaultMaxDecimal); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, Constants.DEFAULT_NUMBER_DECIMAL_SEPARATOR); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, Constants.DEFAULT_NUMBER_THOUSAND_SEPARATOR); - const wrapNegativeNumber = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, Constants.DEFAULT_NEGATIVE_NUMBER_WRAPPED_IN_BRAQUET); - const currencyPrefix = getValueFromParamsOrFormatterOptions('currencyPrefix', columnDef, grid, ''); - const currencySuffix = getValueFromParamsOrFormatterOptions('currencySuffix', columnDef, grid, ''); + const gridOptions = ((grid && typeof grid.getOptions === 'function') ? grid.getOptions() : {}) as GridOption; + const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, gridOptions, defaultMinDecimal); + const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, gridOptions, defaultMaxDecimal); + const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, gridOptions, Constants.DEFAULT_NUMBER_DECIMAL_SEPARATOR); + const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, gridOptions, Constants.DEFAULT_NUMBER_THOUSAND_SEPARATOR); + const wrapNegativeNumber = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, gridOptions, Constants.DEFAULT_NEGATIVE_NUMBER_WRAPPED_IN_BRAQUET); + const currencyPrefix = getValueFromParamsOrFormatterOptions('currencyPrefix', columnDef, gridOptions, ''); + const currencySuffix = getValueFromParamsOrFormatterOptions('currencySuffix', columnDef, gridOptions, ''); if (formatterType === 'cell') { - numberPrefix = getValueFromParamsOrFormatterOptions('numberPrefix', columnDef, grid, ''); - numberSuffix = getValueFromParamsOrFormatterOptions('numberSuffix', columnDef, grid, ''); + numberPrefix = getValueFromParamsOrFormatterOptions('numberPrefix', columnDef, gridOptions, ''); + numberSuffix = getValueFromParamsOrFormatterOptions('numberSuffix', columnDef, gridOptions, ''); } return { minDecimal, maxDecimal, decimalSeparator, thousandSeparator, wrapNegativeNumber, currencyPrefix, currencySuffix, numberPrefix, numberSuffix }; @@ -81,8 +82,7 @@ export function retrieveFormatterOptions(columnDef: Column, grid: SlickGrid, num * 2- Grid Options "formatterOptions" * 3- nothing found, return default value provided */ -export function getValueFromParamsOrFormatterOptions(optionName: string, columnDef: Column, grid: SlickGrid, defaultValue?: any) { - const gridOptions = ((grid && typeof grid.getOptions === 'function') ? grid.getOptions() : {}) as GridOption; +export function getValueFromParamsOrFormatterOptions(optionName: string, columnDef: Column, gridOptions: GridOption, defaultValue?: any) { const params = columnDef && columnDef.params; if (params && params.hasOwnProperty(optionName)) { diff --git a/packages/common/src/global-grid-options.ts b/packages/common/src/global-grid-options.ts index 977be6af4..a46befc88 100644 --- a/packages/common/src/global-grid-options.ts +++ b/packages/common/src/global-grid-options.ts @@ -157,7 +157,7 @@ export const GlobalGridOptions: GridOption = { groupCollapsedSymbol: '⮞', groupExpandedSymbol: '⮟', groupingAggregatorRowText: '', - sanitizeDataExport: false, + sanitizeDataExport: true, }, textExportOptions: { delimiter: DelimiterType.comma, @@ -166,7 +166,7 @@ export const GlobalGridOptions: GridOption = { format: FileType.csv, groupingColumnHeaderTitle: 'Group By', groupingAggregatorRowText: '', - sanitizeDataExport: false, + sanitizeDataExport: true, useUtf8WithBom: true }, gridAutosizeColsMode: GridAutosizeColsMode.none, diff --git a/packages/common/src/interfaces/columnExcelExportOption.interface.ts b/packages/common/src/interfaces/columnExcelExportOption.interface.ts index 7e6cb4991..7aabf0946 100644 --- a/packages/common/src/interfaces/columnExcelExportOption.interface.ts +++ b/packages/common/src/interfaces/columnExcelExportOption.interface.ts @@ -1,5 +1,6 @@ import { Column } from './column.interface'; import { ExcelCellFormat } from './excelCellFormat.interface'; +import { GridOption } from './gridOption.interface'; /** Excel custom export options (formatting & width) that can be applied to a column */ export interface ColumnExcelExportOption { @@ -27,7 +28,7 @@ export interface GroupTotalExportOption { valueParserCallback?: GetGroupTotalValueCallback; } -export type GetDataValueCallback = (data: Date | string | number, columnDef: Column, excelFormatterId: number | undefined, excelStylesheet: unknown) => Date | string | number | ExcelCellFormat; +export type GetDataValueCallback = (data: Date | string | number, columnDef: Column, excelFormatterId: number | undefined, excelStylesheet: unknown, gridOptions: GridOption) => Date | string | number | ExcelCellFormat; export type GetGroupTotalValueCallback = (totals: any, columnDef: Column, groupType: string, excelStylesheet: unknown) => Date | string | number; /** diff --git a/packages/excel-export/src/excelExport.service.spec.ts b/packages/excel-export/src/excelExport.service.spec.ts index eb8b42f57..5d8d5be8e 100644 --- a/packages/excel-export/src/excelExport.service.spec.ts +++ b/packages/excel-export/src/excelExport.service.spec.ts @@ -819,13 +819,14 @@ describe('ExcelExportService', () => { let mockItem1; let mockItem2; let mockGroup1; - let parserCallbackSpy = jest.fn(); - let groupTotalParserCallbackSpy = jest.fn(); + const parserCallbackSpy = jest.fn(); + const groupTotalParserCallbackSpy = jest.fn(); beforeEach(() => { mockGridOptions.enableGrouping = true; mockGridOptions.enableTranslate = false; mockGridOptions.excelExportOptions = { sanitizeDataExport: true, addGroupIndentation: true }; + mockGridOptions.formatterOptions = { decimalSeparator: ',' }; mockColumns = [ { id: 'id', field: 'id', excludeFromExport: true }, @@ -863,7 +864,7 @@ describe('ExcelExportService', () => { }; mockItem1 = { id: 0, userId: '1E06', firstName: 'John', lastName: 'X', position: 'SALES_REP', order: 10, cost: 22 }; - mockItem2 = { id: 1, userId: '2B02', firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 10, cost: 33 }; + mockItem2 = { id: 1, userId: '2B02', firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 10, cost: '$33,01' }; mockGroup1 = { collapsed: 0, count: 2, groupingKey: '10', groups: null, level: 0, selectChecked: false, rows: [mockItem1, mockItem2], @@ -908,8 +909,8 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Cost', }, ], ['⮟ Order: 20 (2 items)'], - ['', '1E06', 'John', 'X', 'SALES_REP', { metadata: { style: 3, type: "number", }, value: 10, }, 8888], - ['', '2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', { metadata: { style: 3, type: "number", }, value: 10, }, 8888], + ['', '1E06', 'John', 'X', 'SALES_REP', { metadata: { style: 3, type: 'number', }, value: 10, }, 8888], + ['', '2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', { metadata: { style: 3, type: 'number', }, value: 10, }, 8888], ['', '', '', '', '', { value: 20, metadata: { style: 5, type: 'number' } }, ''], ] }); @@ -921,7 +922,7 @@ describe('ExcelExportService', () => { numFmtId: 103, } }); - expect(parserCallbackSpy).toHaveBeenCalledWith(22, mockColumns[6], undefined, expect.anything()); + expect(parserCallbackSpy).toHaveBeenCalledWith(22, mockColumns[6], undefined, expect.anything(), mockGridOptions); }); }); @@ -1028,7 +1029,7 @@ describe('ExcelExportService', () => { let mockGroup2; let mockGroup3; let mockGroup4; - let groupTotalParserCallbackSpy = jest.fn(); + const groupTotalParserCallbackSpy = jest.fn(); beforeEach(() => { mockGridOptions.enableGrouping = true; diff --git a/packages/excel-export/src/excelExport.service.ts b/packages/excel-export/src/excelExport.service.ts index 54babc94a..bd0fe44a7 100644 --- a/packages/excel-export/src/excelExport.service.ts +++ b/packages/excel-export/src/excelExport.service.ts @@ -239,7 +239,7 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ * 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[] }) { + startDownloadFile(options: { filename: string, blob: Blob, data: any[]; }) { // when using IE/Edge, then use different download call if (typeof (navigator as any).msSaveOrOpenBlob === 'function') { (navigator as any).msSaveOrOpenBlob(options.blob, options.filename); @@ -583,14 +583,15 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ } this._regularCellExcelFormats[columnDef.id] = cellStyleFormat; } - const { stylesheetFormatterId, getDataValueParser } = this._regularCellExcelFormats[columnDef.id]; - itemData = getDataValueParser(itemData, columnDef, stylesheetFormatterId, this._stylesheet); - // does the user want to sanitize the output data (remove HTML tags)? + // sanitize early, when enabled, any HTML tags (remove HTML tags) if (typeof itemData === 'string' && (columnDef.sanitizeDataExport || this._excelExportOptions.sanitizeDataExport)) { itemData = sanitizeHtmlToText(itemData as string); } + const { stylesheetFormatterId, getDataValueParser } = this._regularCellExcelFormats[columnDef.id]; + itemData = getDataValueParser(itemData, columnDef, stylesheetFormatterId, this._stylesheet, this._gridOptions); + rowOutputStrings.push(itemData); idx++; } diff --git a/packages/excel-export/src/excelUtils.spec.ts b/packages/excel-export/src/excelUtils.spec.ts index c938cbe35..482fe52e6 100644 --- a/packages/excel-export/src/excelUtils.spec.ts +++ b/packages/excel-export/src/excelUtils.spec.ts @@ -1,6 +1,6 @@ import { Column, ExcelStylesheet, FieldType, Formatters, GridOption, GroupTotalFormatters, SlickGrid } from '@slickgrid-universal/common'; -import { getExcelFormatFromGridFormatter, getNumericFormatterOptions, isColumnDateType, useCellFormatByFieldType } from './excelUtils'; +import { getExcelFormatFromGridFormatter, getExcelNumberCallback, getNumericFormatterOptions, useCellFormatByFieldType } from './excelUtils'; const mockGridOptions = { enableExcelExport: true, @@ -20,7 +20,7 @@ const stylesheetStub = { } as unknown as ExcelStylesheet; describe('excelUtils', () => { - let mockedFormatId = 135; + const mockedFormatId = 135; let createFormatSpy: any; beforeEach(() => { @@ -31,6 +31,32 @@ describe('excelUtils', () => { jest.clearAllMocks(); }); + describe('getExcelNumberCallback() method', () => { + it('should return same data when input not a number', () => { + const output = getExcelNumberCallback('something else', {} as Column, 3, {}, mockGridOptions); + expect(output).toEqual({ metadata: { style: 3 }, value: 'something else' }); + }); + + it('should return same data when input value is already a number', () => { + const output = getExcelNumberCallback(9.33, {} as Column, 3, {}, mockGridOptions); + expect(output).toEqual({ metadata: { style: 3 }, value: 9.33 }); + }); + + it('should return parsed number when input value can be parsed to a number', () => { + const output = getExcelNumberCallback('$1,209.33', {} as Column, 3, {}, mockGridOptions); + expect(output).toEqual({ metadata: { style: 3 }, value: 1209.33 }); + }); + + it('should be able to provide a number with different decimal separator as formatter options and return parsed number when input value can be parsed to a number', () => { + const output = getExcelNumberCallback( + '1 244 209,33€', {} as Column, 3, {}, + { + ...mockGridOptions, formatterOptions: { decimalSeparator: ',', thousandSeparator: ' ' } + }); + expect(output).toEqual({ metadata: { style: 3 }, value: 1244209.33 }); + }); + }); + describe('decimal formatter', () => { afterEach(() => { jest.clearAllMocks(); @@ -341,6 +367,26 @@ describe('excelUtils', () => { }); }); + it('should get formatter options for Formatters.dollarColoredBold when using Formatters.multiple and 1 of its formatter is dollarColoredBold formatter', () => { + const column = { + type: FieldType.number, formatter: Formatters.multiple, + params: { formatters: [Formatters.dollarColoredBold, Formatters.bold], displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'cell'); + + expect(output).toEqual({ + currencyPrefix: '', + currencySuffix: '', + decimalSeparator: '.', + maxDecimal: 4, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: ',', + wrapNegativeNumber: true, + }); + }); + it('should get formatter options for Formatters.dollarColored', () => { const column = { type: FieldType.number, formatter: Formatters.dollarColored, @@ -401,6 +447,26 @@ describe('excelUtils', () => { }); }); + it('should get formatter options for Formatters.percent when using Formatters.multiple and 1 of its formatter is percent formatter', () => { + const column = { + type: FieldType.number, formatter: Formatters.multiple, + params: { formatters: [Formatters.percent, Formatters.bold], displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'cell'); + + expect(output).toEqual({ + currencyPrefix: '', + currencySuffix: '', + decimalSeparator: '.', + maxDecimal: undefined, + minDecimal: undefined, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: ',', + wrapNegativeNumber: true, + }); + }); + it('should get formatter options for Formatters.percentComplete', () => { const column = { type: FieldType.number, formatter: Formatters.percentComplete, @@ -611,7 +677,8 @@ describe('excelUtils', () => { it('should get excel excel metadata style with regular number format when a custom GroupTotalFormatters is provided', () => { const columnDef = { type: FieldType.number, formatter: Formatters.decimal, - groupTotalsFormatter: (totals: any, columnDef: Column, grid: SlickGrid) => `Some Total: ${totals.sum}`, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + groupTotalsFormatter: (totals: any, _columnDef: Column, _grid: SlickGrid) => `Some Total: ${totals.sum}`, } as Column; const output = getExcelFormatFromGridFormatter(stylesheetStub, { numberFormatter: { id: 3 } }, columnDef, gridStub, 'group'); @@ -716,6 +783,39 @@ describe('excelUtils', () => { expect(output).toEqual({ groupType: '', stylesheetFormatter: { id: 3 } }); }); + + it('should get excel excel metadata style with regular number format when using Formatters.multiple and a custom Formatter is provided', () => { + const columnDef = { + type: FieldType.number, + formatter: Formatters.multiple, + params: { formatters: [() => `Something rendered`, Formatters.bold], }, + } as unknown as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, { numberFormatter: { id: 3 } }, columnDef, gridStub, 'cell'); + + expect(output).toEqual({ groupType: '', stylesheetFormatter: { id: 3 } }); + }); + + it('should get excel excel metadata style format for Formatters.currency when using Formatters.multiple and the first multiple formatters is currency formatter', () => { + const column = { + type: FieldType.number, + formatter: Formatters.multiple, + params: { formatters: [Formatters.currency, Formatters.bold], displayNegativeNumberWithParentheses: false, thousandSeparator: ' ' } + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'cell'); + + expect(output).toEqual({ groupType: '', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for Formatters.dollar when using Formatters.multiple and the last formatter is dollar formatter', () => { + const column = { + type: FieldType.number, + formatter: Formatters.multiple, + params: { formatters: [Formatters.bold, Formatters.dollar], displayNegativeNumberWithParentheses: false, thousandSeparator: ' ' } + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'cell'); + + expect(output).toEqual({ groupType: '', stylesheetFormatter: { id: 135 } }); + }); }); }); }); \ No newline at end of file diff --git a/packages/excel-export/src/excelUtils.ts b/packages/excel-export/src/excelUtils.ts index db05fb9fe..f56589071 100644 --- a/packages/excel-export/src/excelUtils.ts +++ b/packages/excel-export/src/excelUtils.ts @@ -1,13 +1,16 @@ import { Column, + Constants, ExcelStylesheet, FieldType, + Formatter, Formatters, FormatterType, getColumnFieldType, GetDataValueCallback, + getValueFromParamsOrFormatterOptions, + GridOption, GroupTotalFormatters, - isNumber, retrieveFormatterOptions, sanitizeHtmlToText, SlickGrid, @@ -17,11 +20,24 @@ export type ExcelFormatter = object & { id: number; }; // define all type of potential excel data function callbacks export const getExcelSameInputDataCallback: GetDataValueCallback = (data) => data; -export const getExcelNumberCallback: GetDataValueCallback = (data, _col, excelFormatterId) => ({ - value: isNumber(data) ? +data : data, +export const getExcelNumberCallback: GetDataValueCallback = (data, column, excelFormatterId, _excelSheet, gridOptions) => ({ + value: typeof data === 'string' && /\d/g.test(data) ? parseNumberWithFormatterOptions(data, column, gridOptions) : data, metadata: { style: excelFormatterId } }); +/** Parse a number which the user might have provided formatter options (for example a user might have provided { decimalSeparator: ',', thousandSeparator: ' '}) */ +export function parseNumberWithFormatterOptions(value: any, column: Column, gridOptions: GridOption) { + let outValue = value; + if (typeof value === 'string' && value) { + const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', column, gridOptions, Constants.DEFAULT_NUMBER_DECIMAL_SEPARATOR); + const val: number | string = (decimalSeparator === ',') + ? parseFloat(value.replace(/[^0-9\,]+/g, '').replace(',', '.')) + : parseFloat(value.replace(/[^\d\.]/g, '')); + outValue = isNaN(val) ? value : val; + } + return outValue; +} + /** use different Excel Stylesheet Format as per the Field Type */ export function useCellFormatByFieldType(stylesheet: ExcelStylesheet, stylesheetFormatters: any, columnDef: Column, grid: SlickGrid) { const fieldType = getColumnFieldType(columnDef); @@ -71,28 +87,46 @@ export function getNumericFormatterOptions(columnDef: Column, grid: SlickGrid, f break; } } else { - switch (columnDef.formatter) { - case Formatters.currency: - case Formatters.dollar: - case Formatters.dollarColored: - case Formatters.dollarColoredBold: - dataType = 'currency'; - break; - case Formatters.percent: - case Formatters.percentComplete: - case Formatters.percentCompleteBar: - case Formatters.percentCompleteBarWithText: - case Formatters.percentSymbol: - dataType = 'percent'; - break; - case Formatters.decimal: - default: - // use "decimal" instead of "regular" to show optional decimals "##" in Excel - dataType = 'decimal'; - break; + // when formatter is a Formatter.multiple, we need to loop through each of its formatter to find the best numeric data type + if (columnDef.formatter === Formatters.multiple && Array.isArray(columnDef.params?.formatters)) { + dataType = 'decimal'; + for (const formatter of columnDef.params.formatters) { + dataType = getFormatterNumericDataType(formatter); + if (dataType !== 'decimal') { + break; // if we found something different than the default (decimal) then we can assume that we found our type so we can stop & return + } + } + } else { + dataType = getFormatterNumericDataType(columnDef.formatter); } } - return retrieveFormatterOptions(columnDef, grid, dataType, formatterType); + return retrieveFormatterOptions(columnDef, grid, dataType!, formatterType); +} + +export function getFormatterNumericDataType(formatter?: Formatter) { + let dataType: 'currency' | 'decimal' | 'percent' | 'regular'; + + switch (formatter) { + case Formatters.currency: + case Formatters.dollar: + case Formatters.dollarColored: + case Formatters.dollarColoredBold: + dataType = 'currency'; + break; + case Formatters.percent: + case Formatters.percentComplete: + case Formatters.percentCompleteBar: + case Formatters.percentCompleteBarWithText: + case Formatters.percentSymbol: + dataType = 'percent'; + break; + case Formatters.decimal: + default: + // use "decimal" instead of "regular" to show optional decimals "##" in Excel + dataType = 'decimal'; + break; + } + return dataType; } export function getExcelFormatFromGridFormatter(stylesheet: ExcelStylesheet, stylesheetFormatters: any, columnDef: Column, grid: SlickGrid, formatterType: FormatterType) { @@ -134,6 +168,21 @@ export function getExcelFormatFromGridFormatter(stylesheet: ExcelStylesheet, sty switch (fieldType) { case FieldType.number: switch (columnDef.formatter) { + case Formatters.multiple: + // when formatter is a Formatter.multiple, we need to loop through each of its formatter to find the best possible Excel format + if (Array.isArray(columnDef.params?.formatters)) { + for (const formatter of columnDef.params.formatters) { + const { stylesheetFormatter: stylesheetFormatterResult } = getExcelFormatFromGridFormatter(stylesheet, stylesheetFormatters, { ...columnDef, formatter } as Column, grid, formatterType); + if (stylesheetFormatterResult !== stylesheetFormatters.numberFormatter) { + stylesheetFormatter = stylesheetFormatterResult; + break; + } + } + } + if (!stylesheetFormatter) { + stylesheetFormatter = stylesheetFormatters.numberFormatter; + } + break; case Formatters.currency: case Formatters.decimal: case Formatters.dollar: diff --git a/packages/vanilla-force-bundle/src/salesforce-global-grid-options.ts b/packages/vanilla-force-bundle/src/salesforce-global-grid-options.ts index 974fc05e9..e9db5964c 100644 --- a/packages/vanilla-force-bundle/src/salesforce-global-grid-options.ts +++ b/packages/vanilla-force-bundle/src/salesforce-global-grid-options.ts @@ -34,13 +34,12 @@ export const SalesforceGlobalGridOptions = { }, enableExcelExport: true, excelExportOptions: { + exportWithFormatter: true, mimeType: '', // Salesforce doesn't like Excel MIME type (not allowed), but we can bypass the problem by using no type at all sanitizeDataExport: true }, filterTypingDebounce: 250, formatterOptions: { - minDecimal: 0, - maxDecimal: 2, thousandSeparator: ',' }, frozenHeaderWidthCalcDifferential: 2, diff --git a/test/cypress/e2e/example16.cy.ts b/test/cypress/e2e/example16.cy.ts index a5a63c275..1e48b7af5 100644 --- a/test/cypress/e2e/example16.cy.ts +++ b/test/cypress/e2e/example16.cy.ts @@ -1,5 +1,5 @@ describe('Example 16 - Regular & Custom Tooltips', { retries: 1 }, () => { - const titles = ['', 'Title', 'Duration', 'Description', 'Description 2', 'Cost', '% Complete', 'Start', 'Finish', 'Effort Driven', 'Prerequisites', 'Action']; + const titles = ['', 'Title', 'Duration', 'Description', 'Description 2', 'Cost (in €)', '% Complete', 'Start', 'Finish', 'Effort Driven', 'Prerequisites', 'Action']; const GRID_ROW_HEIGHT = 33; it('should display Example title', () => {