From 53d77535c556f861dd6b2c3aa6179a0e49b45000 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Sun, 18 Dec 2022 17:14:27 -0500 Subject: [PATCH] feat(exports): add Excel auto-detect format by field types & formatters - this new feature is especially useful with cell as numbers, which prior to this PR was exporting wasn't always exporting cell as numbers, this PR fixes this - note that Date fields will NOT be exported as Date format but rather text fields using MomentJS detected format, dealing with Dates in Excel is just too hard to deal with in code while it's easy enough for the user to convert text fields to dates afterward - Group Total will also be exported as numbers via custom format, for example: `"Total: $"0.00##;"Total: "($0.00##)` so the Total text will show in Excel but the value will actually be saved as a number --- .../src/examples/example02.ts | 10 +- .../src/examples/example07.ts | 1 - packages/common/src/constants.ts | 9 + .../common/src/formatters/decimalFormatter.ts | 20 +- .../formatters/dollarColoredBoldFormatter.ts | 16 +- .../src/formatters/dollarColoredFormatter.ts | 16 +- .../common/src/formatters/dollarFormatter.ts | 16 +- .../src/formatters/formatterUtilities.ts | 44 +- .../formatters/percentCompleteFormatter.ts | 16 +- .../common/src/formatters/percentFormatter.ts | 16 +- .../src/formatters/percentSymbolFormatter.ts | 16 +- packages/common/src/global-grid-options.ts | 1 + .../__tests__/minTotalsFormatter.spec.ts | 54 +- .../__tests__/sumTotalsFormatter.spec.ts | 52 +- .../avgTotalsDollarFormatter.ts | 16 +- .../grouping-formatters/avgTotalsFormatter.ts | 16 +- .../avgTotalsPercentageFormatter.ts | 16 +- .../grouping-formatters/maxTotalsFormatter.ts | 16 +- .../grouping-formatters/minTotalsFormatter.ts | 16 +- .../sumTotalsBoldFormatter.ts | 16 +- .../sumTotalsColoredFormatter.ts | 16 +- .../sumTotalsDollarBoldFormatter.ts | 16 +- .../sumTotalsDollarColoredBoldFormatter.ts | 16 +- .../sumTotalsDollarColoredFormatter.ts | 16 +- .../sumTotalsDollarFormatter.ts | 16 +- .../grouping-formatters/sumTotalsFormatter.ts | 16 +- .../common/src/interfaces/column.interface.ts | 7 + .../interfaces/excelExportOption.interface.ts | 7 + packages/common/src/services/utilities.ts | 6 +- .../src/excelExport.service.spec.ts | 450 ++++------ .../excel-export/src/excelExport.service.ts | 100 ++- packages/excel-export/src/excelUtils.spec.ts | 797 +++++++++++++++--- packages/excel-export/src/excelUtils.ts | 273 ++++-- .../interfaces/excelExportOption.interface.ts | 44 - .../src/interfaces/excelWorkbook.interface.ts | 17 - .../interfaces/excelWorksheet.interface.ts | 36 - packages/excel-export/src/interfaces/index.ts | 3 - 37 files changed, 1423 insertions(+), 796 deletions(-) delete mode 100644 packages/excel-export/src/interfaces/excelExportOption.interface.ts delete mode 100644 packages/excel-export/src/interfaces/excelWorkbook.interface.ts delete mode 100644 packages/excel-export/src/interfaces/excelWorksheet.interface.ts diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts index 61da37bf3..32f0b2b91 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts @@ -117,22 +117,20 @@ export class Example2 { filter: { model: Filters.compoundDate }, sortable: true, type: FieldType.dateIso, + outputType: FieldType.dateIso, formatter: Formatters.dateIso, - exportWithFormatter: true }, { id: 'cost', name: 'Cost', field: 'cost', minWidth: 70, width: 80, - maxWidth: 120, filterable: true, filter: { model: Filters.compoundInputNumber }, type: FieldType.number, sortable: true, - exportWithFormatter: true, - formatter: Formatters.dollar, + formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollar, - params: { groupFormatterPrefix: 'Total: ' /* , groupFormatterSuffix: ' USD' */ } + params: { displayNegativeNumberWithParentheses: true, numberPrefix: '€ ', minDecimal: 2, maxDecimal: 4, groupFormatterPrefix: 'Total: ' /* , groupFormatterSuffix: ' USD' */ }, }, { id: 'effortDriven', name: 'Effort Driven', @@ -169,7 +167,7 @@ export class Example2 { onColumnsChanged: (e, args) => console.log(e, args) }, enableExcelExport: true, - excelExportOptions: { filename: 'my-export', sanitizeDataExport: true }, + excelExportOptions: { filename: 'my-export', sanitizeDataExport: true, exportWithExcelFormat: true, }, textExportOptions: { filename: 'my-export', sanitizeDataExport: true }, registerExternalResources: [this.excelExportService, new TextExportService()], showCustomFooter: true, // display some metrics in the bottom custom footer diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts index 003de9822..b8b23f671 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example07.ts @@ -234,7 +234,6 @@ export class Example7 { showCustomFooter: true, enableExcelExport: true, excelExportOptions: { - exportWithFormatter: true, sanitizeDataExport: true }, enableCellMenu: true, diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index b1be7bf53..882013673 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -78,6 +78,15 @@ export class Constants { TREE_LEVEL_PROP: '__treeLevel', PARENT_PROP: '__parentId', }; + static readonly DEFAULT_FORMATTER_NUMBER_MIN_DECIMAL = 2; + static readonly DEFAULT_FORMATTER_NUMBER_MAX_DECIMAL = 2; + static readonly DEFAULT_FORMATTER_DOLLAR_MIN_DECIMAL = 2; + static readonly DEFAULT_FORMATTER_DOLLAR_MAX_DECIMAL = 4; + static readonly DEFAULT_FORMATTER_PERCENT_MIN_DECIMAL = undefined; + static readonly DEFAULT_FORMATTER_PERCENT_MAX_DECIMAL = undefined; + static readonly DEFAULT_NUMBER_DECIMAL_SEPARATOR = '.'; + static readonly DEFAULT_NUMBER_THOUSAND_SEPARATOR = ''; + static readonly DEFAULT_NEGATIVE_NUMBER_WRAPPED_IN_BRAQUET = false; static readonly SLIDER_DEFAULT_MIN_VALUE = 0; static readonly SLIDER_DEFAULT_MAX_VALUE = 100; static readonly SLIDER_DEFAULT_STEP = 1; diff --git a/packages/common/src/formatters/decimalFormatter.ts b/packages/common/src/formatters/decimalFormatter.ts index c4b4f44fc..522944708 100644 --- a/packages/common/src/formatters/decimalFormatter.ts +++ b/packages/common/src/formatters/decimalFormatter.ts @@ -2,7 +2,7 @@ import { isNumber } from '@slickgrid-universal/utils'; import { Formatter } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from './formatterUtilities'; +import { retrieveFormatterOptions } from './formatterUtilities'; /** * Display the value as x decimals formatted, defaults to 2 decimals. @@ -10,16 +10,18 @@ import { getValueFromParamsOrFormatterOptions } from './formatterUtilities'; * For example:: `{ formatter: Formatters.decimal, params: { minDecimal: 2, maxDecimal: 4 }}` */ export const decimalFormatter: Formatter = (_row, _cell, value, columnDef, _dataContext, grid) => { - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, 2); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, 2); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const numberPrefix = getValueFromParamsOrFormatterOptions('numberPrefix', columnDef, grid, ''); - const numberSuffix = getValueFromParamsOrFormatterOptions('numberSuffix', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + numberPrefix, + numberSuffix, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber, + } = retrieveFormatterOptions(columnDef, grid, 'decimal', 'cell'); if (isNumber(value)) { - return formatNumber(value, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, numberPrefix, numberSuffix, decimalSeparator, thousandSeparator); + return formatNumber(value, minDecimal, maxDecimal, wrapNegativeNumber, numberPrefix, numberSuffix, decimalSeparator, thousandSeparator); } return value; }; diff --git a/packages/common/src/formatters/dollarColoredBoldFormatter.ts b/packages/common/src/formatters/dollarColoredBoldFormatter.ts index 7ce22bf92..acf3ab90e 100644 --- a/packages/common/src/formatters/dollarColoredBoldFormatter.ts +++ b/packages/common/src/formatters/dollarColoredBoldFormatter.ts @@ -2,19 +2,21 @@ import { isNumber } from '@slickgrid-universal/utils'; import { Formatter } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from './formatterUtilities'; +import { retrieveFormatterOptions } from './formatterUtilities'; /** Display the value as 2 decimals formatted with dollar sign '$' at the end of of the value, change color of text to red/green on negative/positive value, show it in bold font weight as well */ export const dollarColoredBoldFormatter: Formatter = (_row, _cell, value, columnDef, _dataContext, grid) => { - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, 2); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, 4); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber, + } = retrieveFormatterOptions(columnDef, grid, 'dollar', 'cell'); if (isNumber(value)) { const colorStyle = (value >= 0) ? 'green' : 'red'; - const formattedNumber = formatNumber(value, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '$', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(value, minDecimal, maxDecimal, wrapNegativeNumber, '$', '', decimalSeparator, thousandSeparator); return `${formattedNumber}`; } return value; diff --git a/packages/common/src/formatters/dollarColoredFormatter.ts b/packages/common/src/formatters/dollarColoredFormatter.ts index ad3799b27..d18ff6523 100644 --- a/packages/common/src/formatters/dollarColoredFormatter.ts +++ b/packages/common/src/formatters/dollarColoredFormatter.ts @@ -2,19 +2,21 @@ import { isNumber } from '@slickgrid-universal/utils'; import { Formatter } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from './formatterUtilities'; +import { retrieveFormatterOptions } from './formatterUtilities'; /** Display the value as 2 decimals formatted with dollar sign '$' at the end of of the value, change color of text to red/green on negative/positive value */ export const dollarColoredFormatter: Formatter = (_row, _cell, value, columnDef, _dataContext, grid) => { - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, 2); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, 4); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber, + } = retrieveFormatterOptions(columnDef, grid, 'dollar', 'cell'); if (isNumber(value)) { const colorStyle = (value >= 0) ? 'green' : 'red'; - const formattedNumber = formatNumber(value, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '$', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(value, minDecimal, maxDecimal, wrapNegativeNumber, '$', '', decimalSeparator, thousandSeparator); return `${formattedNumber}`; } return value; diff --git a/packages/common/src/formatters/dollarFormatter.ts b/packages/common/src/formatters/dollarFormatter.ts index 1d0ac69c0..0b045d5d2 100644 --- a/packages/common/src/formatters/dollarFormatter.ts +++ b/packages/common/src/formatters/dollarFormatter.ts @@ -2,18 +2,20 @@ import { isNumber } from '@slickgrid-universal/utils'; import { Formatter } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from './formatterUtilities'; +import { retrieveFormatterOptions } from './formatterUtilities'; /** Display the value as 2 decimals formatted with dollar sign '$' at the end of of the value */ export const dollarFormatter: Formatter = (_row, _cell, value, columnDef, _dataContext, grid) => { - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, 2); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, 4); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber, + } = retrieveFormatterOptions(columnDef, grid, 'dollar', 'cell'); if (isNumber(value)) { - return formatNumber(value, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '$', '', decimalSeparator, thousandSeparator); + return formatNumber(value, minDecimal, maxDecimal, wrapNegativeNumber, '$', '', decimalSeparator, thousandSeparator); } return value; }; diff --git a/packages/common/src/formatters/formatterUtilities.ts b/packages/common/src/formatters/formatterUtilities.ts index 9cc14defb..c1b0bc990 100644 --- a/packages/common/src/formatters/formatterUtilities.ts +++ b/packages/common/src/formatters/formatterUtilities.ts @@ -4,8 +4,12 @@ import { sanitizeHtmlToText } from '../services/domUtilities'; import { mapMomentDateFormatWithFieldType } from '../services/utilities'; import { multipleFormatter } from './multipleFormatter'; import * as moment_ from 'moment-mini'; +import { Constants } from '../constants'; const moment = (moment_ as any)['default'] || moment_; // patch to fix rollup "moment has no default export" issue, document here https://github.com/rollup/rollup/issues/670 +export type FormatterType = 'group' | 'cell'; +export type NumberType = 'decimal' | 'dollar' | 'percent' | 'regular'; + /** * Automatically add a Custom Formatter on all column definitions that have an Editor. * Instead of manually adding a Custom Formatter on every column definitions that are editables, let's ask the system to do it in an easier automated way. @@ -33,6 +37,42 @@ export function autoAddEditorFormatterToColumnsWithEditor(columnDefinitions: Col } } +export function retrieveFormatterOptions(columnDef: Column, grid: SlickGrid, numberType: NumberType, formatterType: FormatterType) { + let defaultMinDecimal; + let defaultMaxDecimal; + let numberPrefix = ''; + let numberSuffix = ''; + + switch (numberType) { + case 'decimal': + defaultMinDecimal = Constants.DEFAULT_FORMATTER_NUMBER_MIN_DECIMAL; + defaultMaxDecimal = Constants.DEFAULT_FORMATTER_NUMBER_MAX_DECIMAL; + break; + case 'dollar': + defaultMinDecimal = Constants.DEFAULT_FORMATTER_DOLLAR_MIN_DECIMAL; + defaultMaxDecimal = Constants.DEFAULT_FORMATTER_DOLLAR_MAX_DECIMAL; + break; + case 'percent': + defaultMinDecimal = Constants.DEFAULT_FORMATTER_PERCENT_MIN_DECIMAL; + defaultMaxDecimal = Constants.DEFAULT_FORMATTER_PERCENT_MAX_DECIMAL; + break; + 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); + + if (formatterType === 'cell') { + numberPrefix = getValueFromParamsOrFormatterOptions('numberPrefix', columnDef, grid, ''); + numberSuffix = getValueFromParamsOrFormatterOptions('numberSuffix', columnDef, grid, ''); + } + + return { minDecimal, maxDecimal, decimalSeparator, thousandSeparator, wrapNegativeNumber, numberPrefix, numberSuffix }; +} + /** * Find the option value from the following (in order of execution) * 1- Column Definition "params" @@ -96,12 +136,12 @@ export function exportWithFormatterWhenDefined(row: number, col: number let isEvaluatingFormatter = false; // first check if there are any export options provided (as Grid Options) - if (exportOptions && exportOptions.hasOwnProperty('exportWithFormatter')) { + if (exportOptions?.hasOwnProperty('exportWithFormatter')) { isEvaluatingFormatter = !!exportOptions.exportWithFormatter; } // second check if "exportWithFormatter" is provided in the column definition, if so it will have precendence over the Grid Options exportOptions - if (columnDef && columnDef.hasOwnProperty('exportWithFormatter')) { + if (columnDef?.hasOwnProperty('exportWithFormatter')) { isEvaluatingFormatter = !!columnDef.exportWithFormatter; } diff --git a/packages/common/src/formatters/percentCompleteFormatter.ts b/packages/common/src/formatters/percentCompleteFormatter.ts index 6fbadbada..8497b6a58 100644 --- a/packages/common/src/formatters/percentCompleteFormatter.ts +++ b/packages/common/src/formatters/percentCompleteFormatter.ts @@ -2,19 +2,21 @@ import { isNumber } from '@slickgrid-universal/utils'; import { Formatter } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from './formatterUtilities'; +import { retrieveFormatterOptions } from './formatterUtilities'; /** Takes a cell value number (between 0.0-100) and displays a red (<50) or green (>=50) bar */ export const percentCompleteFormatter: Formatter = (_row, _cell, value, columnDef, _dataContext, grid) => { - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber, + } = retrieveFormatterOptions(columnDef, grid, 'percent', 'cell'); if (isNumber(value)) { const colorStyle = (value < 50) ? 'red' : 'green'; - const formattedNumber = formatNumber(value, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '', '%', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(value, minDecimal, maxDecimal, wrapNegativeNumber, '', '%', decimalSeparator, thousandSeparator); const outputFormattedValue = value > 100 ? '100%' : formattedNumber; return `${outputFormattedValue}`; } diff --git a/packages/common/src/formatters/percentFormatter.ts b/packages/common/src/formatters/percentFormatter.ts index b3cc6d29b..a2e44e8ef 100644 --- a/packages/common/src/formatters/percentFormatter.ts +++ b/packages/common/src/formatters/percentFormatter.ts @@ -2,19 +2,21 @@ import { isNumber } from '@slickgrid-universal/utils'; import { Formatter } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from './formatterUtilities'; +import { retrieveFormatterOptions } from './formatterUtilities'; /** Takes a cell value number (between 0.0-1.0) and displays a red (<50) or green (>=50) bar */ export const percentFormatter: Formatter = (_row, _cell, value, columnDef, _dataContext, grid) => { - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber, + } = retrieveFormatterOptions(columnDef, grid, 'percent', 'cell'); if (isNumber(value)) { const percentValue = value * 100; - return formatNumber(percentValue, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '', '%', decimalSeparator, thousandSeparator); + return formatNumber(percentValue, minDecimal, maxDecimal, wrapNegativeNumber, '', '%', decimalSeparator, thousandSeparator); } return value; }; diff --git a/packages/common/src/formatters/percentSymbolFormatter.ts b/packages/common/src/formatters/percentSymbolFormatter.ts index aba4ddd01..637e5cb43 100644 --- a/packages/common/src/formatters/percentSymbolFormatter.ts +++ b/packages/common/src/formatters/percentSymbolFormatter.ts @@ -2,18 +2,20 @@ import { isNumber } from '@slickgrid-universal/utils'; import { Formatter } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from './formatterUtilities'; +import { retrieveFormatterOptions } from './formatterUtilities'; /** Takes a cell value number (between 0-100) and add the "%" after the number */ export const percentSymbolFormatter: Formatter = (_row, _cell, value, columnDef, _dataContext, grid) => { - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber, + } = retrieveFormatterOptions(columnDef, grid, 'percent', 'cell'); if (isNumber(value)) { - return formatNumber(value, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '', '%', decimalSeparator, thousandSeparator); + return formatNumber(value, minDecimal, maxDecimal, wrapNegativeNumber, '', '%', decimalSeparator, thousandSeparator); } return value; }; diff --git a/packages/common/src/global-grid-options.ts b/packages/common/src/global-grid-options.ts index f8c50ab3c..348e185a6 100644 --- a/packages/common/src/global-grid-options.ts +++ b/packages/common/src/global-grid-options.ts @@ -146,6 +146,7 @@ export const GlobalGridOptions: GridOption = { excelExportOptions: { addGroupIndentation: true, exportWithFormatter: false, + exportWithExcelFormat: true, filename: 'export', format: FileType.xlsx, groupingColumnHeaderTitle: 'Group By', diff --git a/packages/common/src/grouping-formatters/__tests__/minTotalsFormatter.spec.ts b/packages/common/src/grouping-formatters/__tests__/minTotalsFormatter.spec.ts index caf44295c..0d6a09d6b 100644 --- a/packages/common/src/grouping-formatters/__tests__/minTotalsFormatter.spec.ts +++ b/packages/common/src/grouping-formatters/__tests__/minTotalsFormatter.spec.ts @@ -8,36 +8,36 @@ describe('minTotalsFormatter', () => { } as unknown as SlickGrid; it('should display an empty string when no value is provided', () => { - const output = minTotalsFormatter({}, {} as Column); + const output = minTotalsFormatter({}, {} as Column, {} as SlickGrid); expect(output).toBe(''); }); it('should display an empty string when the "min" does not find the field property in its object', () => { const columnDef = { id: 'column3', field: 'column3' } as Column; const totals = { min: { column1: 123, column2: 345 } }; - const output = minTotalsFormatter(totals, columnDef, {}); + const output = minTotalsFormatter(totals, columnDef, {} as SlickGrid); expect(output).toBe(''); }); it('should display an empty string when the minimum number is null', () => { const columnDef = { id: 'column1', field: 'column1' } as Column; const totals = { min: { column1: null } }; - const output = minTotalsFormatter(totals, columnDef, {}); + const output = minTotalsFormatter(totals, columnDef, {} as SlickGrid); expect(output).toBe(''); }); it('should display an empty string when the average input is not a number', () => { const columnDef = { id: 'column1', field: 'column1' } as Column; const totals = { min: { column1: 'abc' } }; - const output = minTotalsFormatter(totals, columnDef, {}); + const output = minTotalsFormatter(totals, columnDef, {} as SlickGrid); expect(output).toBe(''); }); it('should display a negative minimum when its input is negative', () => { const totals = { min: { column1: -123, column2: -34.5678, column3: -2.4 } }; - const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1' } as Column, {}); - const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2 } } as Column, {}); + const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1' } as Column, {} as SlickGrid); + const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2 } } as Column, {} as SlickGrid); expect(output1).toBe('-123'); expect(output2).toBe('-34.57'); @@ -46,9 +46,9 @@ describe('minTotalsFormatter', () => { it('should display a negative minimum and thousand separator when its input is negative', () => { const totals = { min: { column1: -12345678, column2: -345678.5678, column3: -2.4 } }; - const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { thousandSeparator: ',' } } as Column, {}); - const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, thousandSeparator: ',' } } as Column, {}); - const output3 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, decimalSeparator: ',', thousandSeparator: '_' } } as Column, {}); + const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { thousandSeparator: ',' } } as Column, {} as SlickGrid); + const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, thousandSeparator: ',' } } as Column, {} as SlickGrid); + const output3 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, decimalSeparator: ',', thousandSeparator: '_' } } as Column, {} as SlickGrid); expect(output1).toBe('-12,345,678'); expect(output2).toBe('-345,678.57'); @@ -58,8 +58,8 @@ describe('minTotalsFormatter', () => { it('should display a negative minimum with parentheses instead of the negative sign when its input is negative', () => { const totals = { min: { column1: -123, column2: -34.5678, column3: -2.4 } }; - const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { displayNegativeNumberWithParentheses: true } } as Column, {}); - const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true } } as Column, {}); + const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { displayNegativeNumberWithParentheses: true } } as Column, {} as SlickGrid); + const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true } } as Column, {} as SlickGrid); expect(output1).toBe('(123)'); expect(output2).toBe('(34.57)'); @@ -68,9 +68,9 @@ describe('minTotalsFormatter', () => { it('should display a negative minimum with thousand separator and parentheses instead of the negative sign when its input is negative', () => { const totals = { min: { column1: -12345678, column2: -345678.5678, column3: -2.4 } }; - const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } } as Column, {}); - const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } } as Column, {}); - const output3 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true, decimalSeparator: ',', thousandSeparator: '_' } } as Column, {}); + const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } } as Column, {} as SlickGrid); + const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } } as Column, {} as SlickGrid); + const output3 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true, decimalSeparator: ',', thousandSeparator: '_' } } as Column, {} as SlickGrid); expect(output1).toBe('(12,345,678)'); expect(output2).toBe('(345,678.57)'); @@ -88,8 +88,8 @@ describe('minTotalsFormatter', () => { it('should display a positive minimum number even when displayNegativeNumberWithParentheses is enabled', () => { const totals = { min: { column1: 123, column2: 34.5678, column3: 2.4 } }; - const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { displayNegativeNumberWithParentheses: true } } as Column, {}); - const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true } } as Column, {}); + const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { displayNegativeNumberWithParentheses: true } } as Column, {} as SlickGrid); + const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true } } as Column, {} as SlickGrid); expect(output1).toBe('123'); expect(output2).toBe('34.57'); @@ -98,8 +98,8 @@ describe('minTotalsFormatter', () => { it('should display the same minimum value when a number with decimals is provided', () => { const totals = { min: { column1: 123.55678, column2: 345.2, column3: -2.45 } }; - const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1' } as Column, {}); - const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2' } as Column, {}); + const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1' } as Column, {} as SlickGrid); + const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2' } as Column, {} as SlickGrid); expect(output1).toBe('123.55678'); expect(output2).toBe('345.2'); @@ -121,9 +121,9 @@ describe('minTotalsFormatter', () => { it('should display a minimum number with user defined minimum & maximum decimal count', () => { const totals = { min: { column1: 123.45678, column2: 345.2, column3: -2.45 } }; - const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2 } } as Column, {}); - const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0 } } as Column, {}); - const output3 = minTotalsFormatter(totals, { id: 'column3', field: 'column3', params: { minDecimal: 3, displayNegativeNumberWithParentheses: true } } as Column, {}); + const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2 } } as Column, {} as SlickGrid); + const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0 } } as Column, {} as SlickGrid); + const output3 = minTotalsFormatter(totals, { id: 'column3', field: 'column3', params: { minDecimal: 3, displayNegativeNumberWithParentheses: true } } as Column, {} as SlickGrid); expect(output1).toBe('123.46'); expect(output2).toBe('345.2'); @@ -133,14 +133,14 @@ describe('minTotalsFormatter', () => { it('should display a minimum number a prefix and suffix', () => { const totals = { min: { column1: 123.45678, column2: 345.2, column3: -2.45 } }; - const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2, groupFormatterPrefix: 'min: ' } } as Column, {}); - const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0, groupFormatterSuffix: ' (max)' } } as Column, {}); + const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2, groupFormatterPrefix: 'min: ' } } as Column, {} as SlickGrid); + const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0, groupFormatterSuffix: ' (max)' } } as Column, {} as SlickGrid); const output3 = minTotalsFormatter( totals, { id: 'column3', field: 'column3', params: { minDecimal: 3, displayNegativeNumberWithParentheses: true, groupFormatterPrefix: 'min: ', groupFormatterSuffix: '/item' } - } as Column + } as Column, {} as SlickGrid ); expect(output1).toBe('min: 123.46'); @@ -151,13 +151,13 @@ describe('minTotalsFormatter', () => { it('should display a sum number with prefix, suffix and thousand separator', () => { const totals = { min: { column1: 12345678.45678, column2: 345678.2, column3: -345678.45 } }; - const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2, groupFormatterPrefix: 'Min: ', decimalSeparator: ',', thousandSeparator: '_' } } as Column, {}); - const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0, groupFormatterSuffix: ' (min)', decimalSeparator: ',', thousandSeparator: '_' } } as Column, {}); + const output1 = minTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2, groupFormatterPrefix: 'Min: ', decimalSeparator: ',', thousandSeparator: '_' } } as Column, {} as SlickGrid); + const output2 = minTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0, groupFormatterSuffix: ' (min)', decimalSeparator: ',', thousandSeparator: '_' } } as Column, {} as SlickGrid); const output3 = minTotalsFormatter( totals, { id: 'column3', field: 'column3', params: { minDecimal: 3, displayNegativeNumberWithParentheses: true, groupFormatterPrefix: 'Min: ', groupFormatterSuffix: '/item', decimalSeparator: ',', thousandSeparator: '_' } - } as Column); + } as Column, {} as SlickGrid); expect(output1).toBe('Min: 12_345_678,46'); expect(output2).toBe('345_678,2 (min)'); diff --git a/packages/common/src/grouping-formatters/__tests__/sumTotalsFormatter.spec.ts b/packages/common/src/grouping-formatters/__tests__/sumTotalsFormatter.spec.ts index 32e605187..9ae7a12e2 100644 --- a/packages/common/src/grouping-formatters/__tests__/sumTotalsFormatter.spec.ts +++ b/packages/common/src/grouping-formatters/__tests__/sumTotalsFormatter.spec.ts @@ -8,37 +8,37 @@ describe('sumTotalsFormatter', () => { } as unknown as SlickGrid; it('should display an empty string when no value is provided', () => { - const output = sumTotalsFormatter({}, {} as Column); + const output = sumTotalsFormatter({}, {} as Column, {} as SlickGrid); expect(output).toBe(''); }); it('should display an empty string when the "sum" does not find the field property in its object', () => { const columnDef = { id: 'column3', field: 'column3' } as Column; const totals = { sum: { column1: 123, column2: 345 } }; - const output = sumTotalsFormatter(totals, columnDef, {}); + const output = sumTotalsFormatter(totals, columnDef, {} as SlickGrid); expect(output).toBe(''); }); it('should display an empty string when the sum property is null', () => { const columnDef = { id: 'column1', field: 'column1' } as Column; const totals = { sum: { column1: null } }; - const output = sumTotalsFormatter(totals, columnDef, {}); + const output = sumTotalsFormatter(totals, columnDef, {} as SlickGrid); expect(output).toBe(''); }); it('should display an empty string when the average input is not a number', () => { const columnDef = { id: 'column1', field: 'column1' } as Column; const totals = { sum: { column1: 'abc' } }; - const output = sumTotalsFormatter(totals, columnDef, {}); + const output = sumTotalsFormatter(totals, columnDef, {} as SlickGrid); expect(output).toBe(''); }); it('should display a negative sum when its input is negative', () => { const totals = { sum: { column1: -123, column2: -34.5678, column3: -2.4 } }; - const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1' } as Column, {}); - const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2 } } as Column, {}); - const output3 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, decimalSeparator: ',' } } as Column, {}); + const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1' } as Column, {} as SlickGrid); + const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2 } } as Column, {} as SlickGrid); + const output3 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, decimalSeparator: ',' } } as Column, {} as SlickGrid); expect(output1).toBe('-123'); expect(output2).toBe('-34.57'); @@ -48,9 +48,9 @@ describe('sumTotalsFormatter', () => { it('should display a negative sum and thousand separator when its input is negative', () => { const totals = { sum: { column1: -12345678, column2: -345678.5678, column3: -2.4 } }; - const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { thousandSeparator: ',' } } as Column, {}); - const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, thousandSeparator: ',' } } as Column, {}); - const output3 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, decimalSeparator: ',', thousandSeparator: '_' } } as Column, {}); + const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { thousandSeparator: ',' } } as Column, {} as SlickGrid); + const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, thousandSeparator: ',' } } as Column, {} as SlickGrid); + const output3 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, decimalSeparator: ',', thousandSeparator: '_' } } as Column, {} as SlickGrid); expect(output1).toBe('-12,345,678'); expect(output2).toBe('-345,678.57'); @@ -60,8 +60,8 @@ describe('sumTotalsFormatter', () => { it('should display a negative sum with parentheses instead of the negative sign when its input is negative', () => { const totals = { sum: { column1: -123, column2: -34.5678, column3: -2.4 } }; - const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { displayNegativeNumberWithParentheses: true } } as Column, {}); - const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true } } as Column, {}); + const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { displayNegativeNumberWithParentheses: true } } as Column, {} as SlickGrid); + const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true } } as Column, {} as SlickGrid); expect(output1).toBe('(123)'); expect(output2).toBe('(34.57)'); @@ -70,9 +70,9 @@ describe('sumTotalsFormatter', () => { it('should display a negative sum with thousand separator and parentheses instead of the negative sign when its input is negative', () => { const totals = { sum: { column1: -12345678, column2: -345678.5678, column3: -2.4 } }; - const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } } as Column, {}); - const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } } as Column, {}); - const output3 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true, decimalSeparator: ',', thousandSeparator: ' ' } } as Column, {}); + const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } } as Column, {} as SlickGrid); + const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } } as Column, {} as SlickGrid); + const output3 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { maxDecimal: 2, displayNegativeNumberWithParentheses: true, decimalSeparator: ',', thousandSeparator: ' ' } } as Column, {} as SlickGrid); expect(output1).toBe('(12,345,678)'); expect(output2).toBe('(345,678.57)'); @@ -90,8 +90,8 @@ describe('sumTotalsFormatter', () => { it('should display the same sum value when a number with decimals is provided', () => { const totals = { sum: { column1: 123.55678, column2: 345.2, column3: -2.45 } }; - const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1' } as Column, {}); - const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2' } as Column, {}); + const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1' } as Column, {} as SlickGrid); + const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2' } as Column, {} as SlickGrid); expect(output1).toBe('123.55678'); expect(output2).toBe('345.2'); @@ -100,9 +100,9 @@ describe('sumTotalsFormatter', () => { it('should display a sum number with user defined minimum & maximum decimal count', () => { const totals = { sum: { column1: 123.45678, column2: 345.2, column3: -2.45 } }; - const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2 } } as Column, {}); - const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0 } } as Column, {}); - const output3 = sumTotalsFormatter(totals, { id: 'column3', field: 'column3', params: { minDecimal: 3, displayNegativeNumberWithParentheses: true } } as Column, {}); + const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2 } } as Column, {} as SlickGrid); + const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0 } } as Column, {} as SlickGrid); + const output3 = sumTotalsFormatter(totals, { id: 'column3', field: 'column3', params: { minDecimal: 3, displayNegativeNumberWithParentheses: true } } as Column, {} as SlickGrid); expect(output1).toBe('123.46'); expect(output2).toBe('345.2'); @@ -125,14 +125,14 @@ describe('sumTotalsFormatter', () => { it('should display a sum number a prefix and suffix', () => { const totals = { sum: { column1: 123.45678, column2: 345.2, column3: -2.45 } }; - const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2, groupFormatterPrefix: 'sum: ' } } as Column, {}); - const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0, groupFormatterSuffix: ' (max)' } } as Column, {}); + const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2, groupFormatterPrefix: 'sum: ' } } as Column, {} as SlickGrid); + const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0, groupFormatterSuffix: ' (max)' } } as Column, {} as SlickGrid); const output3 = sumTotalsFormatter( totals, { id: 'column3', field: 'column3', params: { minDecimal: 3, displayNegativeNumberWithParentheses: true, groupFormatterPrefix: 'sum: ', groupFormatterSuffix: '/item' } - } as Column + } as Column, {} as SlickGrid ); expect(output1).toBe('sum: 123.46'); @@ -143,13 +143,13 @@ describe('sumTotalsFormatter', () => { it('should display a sum number with prefix, suffix and thousand separator', () => { const totals = { sum: { column1: 12345678.45678, column2: 345678.2, column3: -345678.45 } }; - const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2, groupFormatterPrefix: 'Sum: ', decimalSeparator: ',', thousandSeparator: '_' } } as Column, {}); - const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0, groupFormatterSuffix: ' (sum)', decimalSeparator: ',', thousandSeparator: '_' } } as Column, {}); + const output1 = sumTotalsFormatter(totals, { id: 'column1', field: 'column1', params: { maxDecimal: 2, groupFormatterPrefix: 'Sum: ', decimalSeparator: ',', thousandSeparator: '_' } } as Column, {} as SlickGrid); + const output2 = sumTotalsFormatter(totals, { id: 'column2', field: 'column2', params: { minDecimal: 0, groupFormatterSuffix: ' (sum)', decimalSeparator: ',', thousandSeparator: '_' } } as Column, {} as SlickGrid); const output3 = sumTotalsFormatter( totals, { id: 'column3', field: 'column3', params: { minDecimal: 3, displayNegativeNumberWithParentheses: true, groupFormatterPrefix: 'Sum: ', groupFormatterSuffix: '/item', decimalSeparator: ',', thousandSeparator: '_' } - } as Column); + } as Column, {} as SlickGrid); expect(output1).toBe('Sum: 12_345_678,46'); expect(output2).toBe('345_678,2 (sum)'); diff --git a/packages/common/src/grouping-formatters/avgTotalsDollarFormatter.ts b/packages/common/src/grouping-formatters/avgTotalsDollarFormatter.ts index 17cea0145..3e5cf6798 100644 --- a/packages/common/src/grouping-formatters/avgTotalsDollarFormatter.ts +++ b/packages/common/src/grouping-formatters/avgTotalsDollarFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; export const avgTotalsDollarFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,14 +8,16 @@ export const avgTotalsDollarFormatter: GroupTotalsFormatter = (totals: any, colu const params = columnDef?.params; const prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, 2); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, 4); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'dollar', 'group'); if (val !== null && !isNaN(+val)) { - const formattedNumber = formatNumber(val, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '$', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(val, minDecimal, maxDecimal, wrapNegativeNumber, '$', '', decimalSeparator, thousandSeparator); return `${prefix}${formattedNumber}${suffix}`; } return ''; diff --git a/packages/common/src/grouping-formatters/avgTotalsFormatter.ts b/packages/common/src/grouping-formatters/avgTotalsFormatter.ts index 0b3e4e54d..8baee2e33 100644 --- a/packages/common/src/grouping-formatters/avgTotalsFormatter.ts +++ b/packages/common/src/grouping-formatters/avgTotalsFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { decimalFormatted, thousandSeparatorFormatted } from '../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const avgTotalsFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,16 +8,18 @@ export const avgTotalsFormatter: GroupTotalsFormatter = (totals: any, columnDef: const params = columnDef?.params; let prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'regular', 'group'); if (val !== null && !isNaN(+val)) { if (val < 0) { val = Math.abs(val); - if (!displayNegativeNumberWithParentheses) { + if (!wrapNegativeNumber) { prefix += '-'; } else { if (isNaN(minDecimal) && isNaN(maxDecimal)) { diff --git a/packages/common/src/grouping-formatters/avgTotalsPercentageFormatter.ts b/packages/common/src/grouping-formatters/avgTotalsPercentageFormatter.ts index e15a2c6f8..b82ee9d38 100644 --- a/packages/common/src/grouping-formatters/avgTotalsPercentageFormatter.ts +++ b/packages/common/src/grouping-formatters/avgTotalsPercentageFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { decimalFormatted, thousandSeparatorFormatted } from '../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const avgTotalsPercentageFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,16 +8,18 @@ export const avgTotalsPercentageFormatter: GroupTotalsFormatter = (totals: any, const params = columnDef?.params; let prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'percent', 'group'); if (val !== null && !isNaN(+val)) { if (val < 0) { val = Math.abs(val); - if (!displayNegativeNumberWithParentheses) { + if (!wrapNegativeNumber) { prefix += '-'; } else { if (isNaN(minDecimal) && isNaN(maxDecimal)) { diff --git a/packages/common/src/grouping-formatters/maxTotalsFormatter.ts b/packages/common/src/grouping-formatters/maxTotalsFormatter.ts index 63eb7bfa3..b436cdb51 100644 --- a/packages/common/src/grouping-formatters/maxTotalsFormatter.ts +++ b/packages/common/src/grouping-formatters/maxTotalsFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { formatNumber } from '../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const maxTotalsFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,14 +8,16 @@ export const maxTotalsFormatter: GroupTotalsFormatter = (totals: any, columnDef: const params = columnDef?.params; const prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'regular', 'group'); if (val !== null && !isNaN(+val)) { - const formattedNumber = formatNumber(val, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(val, minDecimal, maxDecimal, wrapNegativeNumber, '', '', decimalSeparator, thousandSeparator); return `${prefix}${formattedNumber}${suffix}`; } return ''; diff --git a/packages/common/src/grouping-formatters/minTotalsFormatter.ts b/packages/common/src/grouping-formatters/minTotalsFormatter.ts index 875ff44e6..579ec1777 100644 --- a/packages/common/src/grouping-formatters/minTotalsFormatter.ts +++ b/packages/common/src/grouping-formatters/minTotalsFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { formatNumber } from '../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const minTotalsFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,14 +8,16 @@ export const minTotalsFormatter: GroupTotalsFormatter = (totals: any, columnDef: const params = columnDef?.params; const prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'regular', 'group'); if (val !== null && !isNaN(+val)) { - const formattedNumber = formatNumber(val, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(val, minDecimal, maxDecimal, wrapNegativeNumber, '', '', decimalSeparator, thousandSeparator); return `${prefix}${formattedNumber}${suffix}`; } return ''; diff --git a/packages/common/src/grouping-formatters/sumTotalsBoldFormatter.ts b/packages/common/src/grouping-formatters/sumTotalsBoldFormatter.ts index 044a7f0dc..28f22cb65 100644 --- a/packages/common/src/grouping-formatters/sumTotalsBoldFormatter.ts +++ b/packages/common/src/grouping-formatters/sumTotalsBoldFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { formatNumber } from '../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const sumTotalsBoldFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,14 +8,16 @@ export const sumTotalsBoldFormatter: GroupTotalsFormatter = (totals: any, column const params = columnDef?.params; const prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'regular', 'group'); if (val !== null && !isNaN(+val)) { - const formattedNumber = formatNumber(val, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(val, minDecimal, maxDecimal, wrapNegativeNumber, '', '', decimalSeparator, thousandSeparator); return `${prefix}${formattedNumber}${suffix}`; } return ''; diff --git a/packages/common/src/grouping-formatters/sumTotalsColoredFormatter.ts b/packages/common/src/grouping-formatters/sumTotalsColoredFormatter.ts index 2c7bacc75..e497d71ac 100644 --- a/packages/common/src/grouping-formatters/sumTotalsColoredFormatter.ts +++ b/packages/common/src/grouping-formatters/sumTotalsColoredFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { formatNumber } from '../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const sumTotalsColoredFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,15 +8,17 @@ export const sumTotalsColoredFormatter: GroupTotalsFormatter = (totals: any, col const params = columnDef?.params; const prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'regular', 'group'); if (val !== null && !isNaN(+val)) { const colorStyle = (val >= 0) ? 'green' : 'red'; - const formattedNumber = formatNumber(val, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(val, minDecimal, maxDecimal, wrapNegativeNumber, '', '', decimalSeparator, thousandSeparator); return `${prefix}${formattedNumber}${suffix}`; } return ''; diff --git a/packages/common/src/grouping-formatters/sumTotalsDollarBoldFormatter.ts b/packages/common/src/grouping-formatters/sumTotalsDollarBoldFormatter.ts index 6e44f9e04..ff993aeb1 100644 --- a/packages/common/src/grouping-formatters/sumTotalsDollarBoldFormatter.ts +++ b/packages/common/src/grouping-formatters/sumTotalsDollarBoldFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const sumTotalsDollarBoldFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,14 +8,16 @@ export const sumTotalsDollarBoldFormatter: GroupTotalsFormatter = (totals: any, const params = columnDef?.params; const prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, 2); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, 4); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'dollar', 'group'); if (val !== null && !isNaN(+val)) { - const formattedNumber = formatNumber(val, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '$', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(val, minDecimal, maxDecimal, wrapNegativeNumber, '$', '', decimalSeparator, thousandSeparator); return `${prefix}${formattedNumber}${suffix}`; } return ''; diff --git a/packages/common/src/grouping-formatters/sumTotalsDollarColoredBoldFormatter.ts b/packages/common/src/grouping-formatters/sumTotalsDollarColoredBoldFormatter.ts index bddc96455..33b2e27f9 100644 --- a/packages/common/src/grouping-formatters/sumTotalsDollarColoredBoldFormatter.ts +++ b/packages/common/src/grouping-formatters/sumTotalsDollarColoredBoldFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const sumTotalsDollarColoredBoldFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,15 +8,17 @@ export const sumTotalsDollarColoredBoldFormatter: GroupTotalsFormatter = (totals const params = columnDef?.params; const prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, 2); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, 4); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'dollar', 'group'); if (val !== null && !isNaN(+val)) { const colorStyle = (val >= 0) ? 'green' : 'red'; - const formattedNumber = formatNumber(val, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '$', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(val, minDecimal, maxDecimal, wrapNegativeNumber, '$', '', decimalSeparator, thousandSeparator); return `${prefix}${formattedNumber}${suffix}`; } return ''; diff --git a/packages/common/src/grouping-formatters/sumTotalsDollarColoredFormatter.ts b/packages/common/src/grouping-formatters/sumTotalsDollarColoredFormatter.ts index b5644329c..582c584e7 100644 --- a/packages/common/src/grouping-formatters/sumTotalsDollarColoredFormatter.ts +++ b/packages/common/src/grouping-formatters/sumTotalsDollarColoredFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const sumTotalsDollarColoredFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,15 +8,17 @@ export const sumTotalsDollarColoredFormatter: GroupTotalsFormatter = (totals: an const params = columnDef?.params; const prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, 2); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, 4); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'dollar', 'group'); if (val !== null && !isNaN(+val)) { const colorStyle = (val >= 0) ? 'green' : 'red'; - const formattedNumber = formatNumber(val, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '$', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(val, minDecimal, maxDecimal, wrapNegativeNumber, '$', '', decimalSeparator, thousandSeparator); return `${prefix}${formattedNumber}${suffix}`; } return ''; diff --git a/packages/common/src/grouping-formatters/sumTotalsDollarFormatter.ts b/packages/common/src/grouping-formatters/sumTotalsDollarFormatter.ts index edbbee213..3a5da155a 100644 --- a/packages/common/src/grouping-formatters/sumTotalsDollarFormatter.ts +++ b/packages/common/src/grouping-formatters/sumTotalsDollarFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { formatNumber } from './../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const sumTotalsDollarFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,14 +8,16 @@ export const sumTotalsDollarFormatter: GroupTotalsFormatter = (totals: any, colu const params = columnDef?.params; const prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, 2); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, 4); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'dollar', 'group'); if (val !== null && !isNaN(+val)) { - const formattedNumber = formatNumber(val, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '$', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(val, minDecimal, maxDecimal, wrapNegativeNumber, '$', '', decimalSeparator, thousandSeparator); return `${prefix}${formattedNumber}${suffix}`; } return ''; diff --git a/packages/common/src/grouping-formatters/sumTotalsFormatter.ts b/packages/common/src/grouping-formatters/sumTotalsFormatter.ts index 859d00db0..d070229b5 100644 --- a/packages/common/src/grouping-formatters/sumTotalsFormatter.ts +++ b/packages/common/src/grouping-formatters/sumTotalsFormatter.ts @@ -1,6 +1,6 @@ import { Column, GroupTotalsFormatter, SlickGrid } from './../interfaces/index'; import { formatNumber } from '../services/utilities'; -import { getValueFromParamsOrFormatterOptions } from '../formatters/formatterUtilities'; +import { retrieveFormatterOptions } from '../formatters/formatterUtilities'; export const sumTotalsFormatter: GroupTotalsFormatter = (totals: any, columnDef: Column, grid: SlickGrid) => { const field = columnDef.field ?? ''; @@ -8,14 +8,16 @@ export const sumTotalsFormatter: GroupTotalsFormatter = (totals: any, columnDef: const params = columnDef?.params; const prefix = params?.groupFormatterPrefix || ''; const suffix = params?.groupFormatterSuffix || ''; - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); + const { + minDecimal, + maxDecimal, + decimalSeparator, + thousandSeparator, + wrapNegativeNumber + } = retrieveFormatterOptions(columnDef, grid, 'regular', 'group'); if (val !== null && !isNaN(+val)) { - const formattedNumber = formatNumber(val, minDecimal, maxDecimal, displayNegativeNumberWithParentheses, '', '', decimalSeparator, thousandSeparator); + const formattedNumber = formatNumber(val, minDecimal, maxDecimal, wrapNegativeNumber, '', '', decimalSeparator, thousandSeparator); return `${prefix}${formattedNumber}${suffix}`; } return ''; diff --git a/packages/common/src/interfaces/column.interface.ts b/packages/common/src/interfaces/column.interface.ts index 48a1a03b1..0861db19b 100644 --- a/packages/common/src/interfaces/column.interface.ts +++ b/packages/common/src/interfaces/column.interface.ts @@ -115,6 +115,13 @@ export interface Column { */ exportWithFormatter?: boolean; + /** + * Defaults to true, which leads to ExcelExportService trying to detect the best possible Excel format for each cell. + * The difference the other flag is that "exportWithFormatter" will always export as a string, while this option here will try to detect the best Excel format. + * NOTE: Date will still be exported as string, the numbers are the ones taking the best advantage from this option. + */ + exportWithExcelFormat?: boolean; + /** * Do we want to force the cell value to be a string? * When set to True, it will wrap the cell value in double quotes and add an equal sign (=) at the beginning of the cell to force Excel to evaluate it as a string and not change it's format. diff --git a/packages/common/src/interfaces/excelExportOption.interface.ts b/packages/common/src/interfaces/excelExportOption.interface.ts index 449d1ca35..186665d6d 100644 --- a/packages/common/src/interfaces/excelExportOption.interface.ts +++ b/packages/common/src/interfaces/excelExportOption.interface.ts @@ -15,6 +15,13 @@ export interface ExcelExportOption { /** 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; + /** + * Defaults to true, which leads to ExcelExportService trying to detect the best possible Excel format for each cell. + * The difference the other flag is that "exportWithFormatter" will always export as a string, while this option here will try to detect the best Excel format. + * NOTE: Date will still be exported as string, the numbers are the ones taking the best advantage from this option. + */ + exportWithExcelFormat?: boolean; + /** filename (without extension) */ filename?: string; diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts index e0930418b..1d0af98de 100644 --- a/packages/common/src/services/utilities.ts +++ b/packages/common/src/services/utilities.ts @@ -242,13 +242,13 @@ export function decimalFormatted(input: number | string, minDecimal?: number, ma * @param input * @param minDecimal * @param maxDecimal - * @param displayNegativeNumberWithParentheses + * @param wrapNegativeNumberInBraquets * @param symbolPrefix * @param symbolSuffix * @param decimalSeparator * @param thousandSeparator */ -export function formatNumber(input: number | string, minDecimal?: number, maxDecimal?: number, displayNegativeNumberWithParentheses?: boolean, symbolPrefix = '', symbolSuffix = '', decimalSeparator: '.' | ',' = '.', thousandSeparator: ',' | '_' | '.' | ' ' | '' = ''): string { +export function formatNumber(input: number | string, minDecimal?: number, maxDecimal?: number, wrapNegativeNumberInBraquets?: boolean, symbolPrefix = '', symbolSuffix = '', decimalSeparator: '.' | ',' = '.', thousandSeparator: ',' | '_' | '.' | ' ' | '' = ''): string { if (isNaN(+input)) { return input as string; } @@ -257,7 +257,7 @@ export function formatNumber(input: number | string, minDecimal?: number, maxDec if (calculatedValue < 0) { const absValue = Math.abs(calculatedValue); - if (displayNegativeNumberWithParentheses) { + if (wrapNegativeNumberInBraquets) { if (!isNaN(minDecimal as number) || !isNaN(maxDecimal as number)) { return `(${symbolPrefix}${decimalFormatted(absValue, minDecimal, maxDecimal, decimalSeparator, thousandSeparator)}${symbolSuffix})`; } diff --git a/packages/excel-export/src/excelExport.service.spec.ts b/packages/excel-export/src/excelExport.service.spec.ts index 192814398..bdfbe938c 100644 --- a/packages/excel-export/src/excelExport.service.spec.ts +++ b/packages/excel-export/src/excelExport.service.spec.ts @@ -1,4 +1,4 @@ -import moment from 'moment-mini'; +import 'jest-extended'; import { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; import { Column, @@ -191,7 +191,7 @@ describe('ExcelExportService', () => { const optionExpectation = { filename: 'export.xlsx', format: FileType.xlsx }; (navigator as any).msSaveOrOpenBlob = jest.fn(); const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); - const spyMsSave = jest.spyOn(navigator, 'msSaveOrOpenBlob'); + const spyMsSave = jest.spyOn(navigator as any, 'msSaveOrOpenBlob'); service.init(gridStub, container); const result = await service.exportToExcel(mockExportExcelOptions); @@ -206,7 +206,7 @@ describe('ExcelExportService', () => { 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 }]; + mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'X', position: 'SALES_REP', order: 10 }]; jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); @@ -229,7 +229,7 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Position', }, { metadata: { style: 1, }, value: 'Order', }, ], - ['1E06', 'John', 'Z', 'SALES_REP', '10'], + ['1E06', 'John', 'X', 'SALES_REP', '10'], ] }); }); @@ -498,14 +498,14 @@ describe('ExcelExportService', () => { let mockCollection: any[]; beforeEach(() => { - mockGridOptions.excelExportOptions = { sanitizeDataExport: true }; + mockGridOptions.excelExportOptions = { sanitizeDataExport: true, exportWithExcelFormat: 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: 'firstName', field: 'firstName', width: 100, formatter: myBoldHtmlFormatter, exportWithExcelFormat: false }, + { id: 'lastName', field: 'lastName', width: 100, sanitizeDataExport: true, exportWithFormatter: true, exportWithExcelFormat: false }, { id: 'position', field: 'position', width: 100 }, - { id: 'startDate', field: 'startDate', type: FieldType.dateIso, width: 100, exportWithFormatter: false }, + { 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[]; @@ -515,7 +515,7 @@ describe('ExcelExportService', () => { 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: 0, userId: '1E06', firstName: 'John', lastName: 'X', 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); @@ -541,8 +541,8 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'StartDate', }, { metadata: { style: 1, }, value: 'EndDate', }, ], - ['1E06', 'John', 'Z', 'SALES_REP', { metadata: { style: 4, }, value: '2005-12-20' }, ''], - ['1E09', 'Jane', 'Doe', 'HUMAN_RESOURCES', { metadata: { style: 4, }, value: '2010-10-09' }, '2024-01-02'], + ['1E06', 'John', 'X', 'SALES_REP', '2005-12-20', ''], + ['1E09', 'Jane', 'Doe', 'HUMAN_RESOURCES', '2010-10-09', '2024-01-02'], ] }); }); @@ -555,7 +555,7 @@ describe('ExcelExportService', () => { { 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 }, + { id: 'position', field: 'position', width: 100, type: FieldType.number, exportWithExcelFormat: true }, ] as Column[]; jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); @@ -564,7 +564,7 @@ describe('ExcelExportService', () => { 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 }]; + mockCollection = [{ id: 0, user: { firstName: 'John', lastName: 'X' }, position: 'SALES_REP', order: 10 }]; jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); @@ -585,15 +585,15 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Last Name', }, { metadata: { style: 1, }, value: 'Position', }, ], - ['John', 'Z', 'SALES_REP'], + ['John', 'X', { value: 'SALES_REP', metadata: { style: 3 } }], ] }); }); it(`should skip lines that have an empty Slick DataView structure like "getItem" that is null and is part of the item object`, async () => { mockCollection = [ - { id: 0, user: { firstName: 'John', lastName: 'Z' }, position: 'SALES_REP', order: 10 }, - { id: 1, getItem: null, getItems: null, __parent: { id: 0, user: { firstName: 'John', lastName: 'Z' }, position: 'SALES_REP', order: 10 } } + { id: 0, user: { firstName: 'John', lastName: 'X' }, position: 'SALES_REP', order: 10 }, + { id: 1, getItem: null, getItems: null, __parent: { id: 0, user: { firstName: 'John', lastName: 'X' }, position: 'SALES_REP', order: 10 } } ]; jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]).mockReturnValueOnce(mockCollection[1]); @@ -615,7 +615,7 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Last Name', }, { metadata: { style: 1, }, value: 'Position', }, ], - ['John', 'Z', 'SALES_REP'], + ['John', 'X', { value: 'SALES_REP', metadata: { style: 3 } }], ] }); }); @@ -633,7 +633,7 @@ describe('ExcelExportService', () => { { 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: 'position', field: 'position', name: 'Position', type: FieldType.number, width: 100, formatter: Formatters.translate, exportWithFormatter: true }, { id: 'order', field: 'order', width: 100, exportWithFormatter: true, formatter: Formatters.multiple, params: { formatters: [myBoldHtmlFormatter, myCustomObjectFormatter] } }, ] as Column[]; @@ -641,7 +641,7 @@ describe('ExcelExportService', () => { }); it(`should have the LastName header title translated when defined as a "nameKey" and "translater" is set in grid option`, async () => { - mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }]; + mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'X', position: 'SALES_REP', order: 10 }]; jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); @@ -664,7 +664,7 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Position', }, { metadata: { style: 1, }, value: 'Order', }, ], - ['1E06', 'John', 'Z', 'Sales Rep.', '10'], + ['1E06', 'John', 'X', 'Sales Rep.', '10'], ] }); }); @@ -713,7 +713,7 @@ describe('ExcelExportService', () => { predefinedValues: [], }; - mockItem1 = { id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }; + mockItem1 = { id: 0, userId: '1E06', firstName: 'John', lastName: 'X', 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, @@ -757,7 +757,7 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Order', }, ], ['⮟ Order: 20 (2 items)'], - ['', '1E06', 'John', 'Z', 'SALES_REP', '10'], + ['', '1E06', 'John', 'X', 'SALES_REP', '10'], ['', '2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', '10'], ['', '', '', '', '', 'Custom: 20'], ] @@ -765,6 +765,103 @@ describe('ExcelExportService', () => { }); }); + describe('with Grouping and export with Excel custom format', () => { + let mockCollection: any[]; + let mockOrderGrouping; + let mockItem1; + let mockItem2; + let mockGroup1; + + beforeEach(() => { + mockGridOptions.enableGrouping = true; + mockGridOptions.enableTranslate = false; + mockGridOptions.excelExportOptions = { sanitizeDataExport: true, addGroupIndentation: true, exportWithExcelFormat: 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, + groupTotalsFormatter: GroupTotalFormatters.avgTotalsDollar, + }, + { + 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: 'X', 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 pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.xlsx', format: 'xlsx' }; + + service.init(gridStub, container); + await service.exportToExcel(mockExportExcelOptions); + + expect(pubSubSpy).toHaveBeenCalledWith(`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', 'X', 'SALES_REP', '10'], + ['', '2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', '10'], + ['', '', '', '', '', { value: 20, metadata: { style: 4, type: 'number' } }], + ] + }); + }); + }); + describe('with Grouping and Translation', () => { let mockCollection: any[]; let mockOrderGrouping; @@ -782,7 +879,7 @@ describe('ExcelExportService', () => { { 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: 'position', field: 'position', name: 'Position', type: FieldType.number, width: 100, formatter: Formatters.translate, exportWithFormatter: true }, { id: 'order', field: 'order', type: FieldType.number, exportWithFormatter: true, @@ -807,7 +904,7 @@ describe('ExcelExportService', () => { predefinedValues: [], }; - mockItem1 = { id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }; + mockItem1 = { id: 0, userId: '1E06', firstName: 'John', lastName: 'X', 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, @@ -851,7 +948,7 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Order', }, ], ['⮟ Order: 20 (2 items)'], - ['', '1E06', 'John', 'Z', 'Sales Rep.', '10'], + ['', '1E06', 'John', 'X', 'Sales Rep.', '10'], ['', '2B02', 'Jane', 'DOE', 'Finance Manager', '10'], ['', '', '', '', '', '20'], ] @@ -904,7 +1001,7 @@ describe('ExcelExportService', () => { predefinedValues: [], }; - mockItem1 = { id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }; + mockItem1 = { id: 0, userId: '1E06', firstName: 'John', lastName: 'X', 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, @@ -913,9 +1010,9 @@ describe('ExcelExportService', () => { 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, + collapsed: false, count: 2, groupingKey: '10:|:X', groups: null, level: 1, selectChecked: false, rows: [mockItem1, mockItem2], - title: `Last Name: Z (1 items)`, + title: `Last Name: X (1 items)`, totals: { value: '10', __group: true, __groupTotals: true, group: {}, initialized: true, sum: { order: 10 } }, }; mockGroup3 = { @@ -974,8 +1071,8 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Order', }, ], ['⮟ Order: 20 (2 items)'], - ['⮟ Last Name: Z (1 items)'], // expanded - ['', '1E06', 'John', 'Z', 'Sales Rep.', '10'], + ['⮟ Last Name: X (1 items)'], // expanded + ['', '1E06', 'John', 'X', 'Sales Rep.', '10'], ['⮟ Last Name: Doe (1 items)'], // expanded ['', '2B02', 'Jane', 'DOE', 'Finance Manager', '10'], ['⮞ Last Name: null (0 items)'], // collapsed @@ -1011,10 +1108,10 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Order', }, ], ['Order: 20 (2 items)'], - ['Last Name: Z (1 items)'], - ['', '1E06', 'John', 'Z', 'Sales Rep.', { metadata: { style: 3, type: 'number' }, value: 10, }], + ['Last Name: X (1 items)'], + ['', '1E06', 'John', 'X', 'Sales Rep.', "10"], ['Last Name: Doe (1 items)'], - ['', '2B02', 'Jane', 'DOE', 'Finance Manager', { metadata: { style: 3, type: 'number' }, value: 10, }], + ['', '2B02', 'Jane', 'DOE', 'Finance Manager', "10"], ['Last Name: null (0 items)'], ['', '', '', '', '', '20'], ['', '', '', '', '', '10'], @@ -1025,279 +1122,13 @@ describe('ExcelExportService', () => { 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'; const column = { type: FieldType.dateTime } as Column; service.init(gridStub, container); await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeIso } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeShortIso } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeIsoAmPm } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeIsoAM_PM } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateEuro } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateEuroShort } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeEuro } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeShortEuro } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeEuroAmPm } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeEuroAM_PM } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeEuroShort } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeEuroShortAmPm } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateUs } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); + const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, column, gridStub); - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateUsShort } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeUs } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeShortUs } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeUsAmPm } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeUsAM_PM } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeUsShort } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateTimeUsShortAmPm } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.dateUtc } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, 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'; - const column = { type: FieldType.date } as Column; - - service.init(gridStub, container); - await service.exportToExcel(mockExportExcelOptions); - const output = useCellFormatByFieldType(service.stylesheet, service.stylesheetFormats, input, column, gridStub); - - expect(output).toEqual({ metadata: { style: 4 }, value: expectedDate }); + expect(output).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); }); @@ -1322,7 +1153,7 @@ describe('ExcelExportService', () => { }); it('should export with grouped header titles showing up on first row', async () => { - mockCollection2 = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }]; + mockCollection2 = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'X', position: 'SALES_REP', order: 10 }]; jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection2.length); jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection2[0]); const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); @@ -1352,7 +1183,7 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Position', }, { metadata: { style: 1, }, value: 'Order', }, ], - ['John', 'Z', '1E06', 'SALES_REP', '10'], + ['John', 'X', '1E06', 'SALES_REP', '10'], ] }); }); @@ -1381,7 +1212,7 @@ describe('ExcelExportService', () => { it(`should have the LastName header title translated when defined as a "headerKey" and "translater" is set in grid option`, async () => { mockGridOptions.excelExportOptions!.sanitizeDataExport = false; - mockTranslateCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }]; + mockTranslateCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'X', position: 'SALES_REP', order: 10 }]; jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockTranslateCollection.length); jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockTranslateCollection[0]); const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); @@ -1411,7 +1242,7 @@ describe('ExcelExportService', () => { { metadata: { style: 1, }, value: 'Position', }, { metadata: { style: 1, }, value: 'Order', }, ], - ['John', 'Z', '1E06', 'Sales Rep.', '10'], + ['John', 'X', '1E06', 'Sales Rep.', '10'], ] }); }); @@ -1429,13 +1260,14 @@ describe('ExcelExportService', () => { mockGridOptions.excelExportOptions = {}; mockGridOptions.createPreHeaderPanel = false; mockGridOptions.showPreHeaderPanel = false; + mockGridOptions.excelExportOptions.exportWithFormatter = true; mockGridOptions.colspanCallback = (item: any) => (item.id % 2 === 1) ? evenMetatadata : oddMetatadata; mockColumns = [ { 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: 'firstName', nameKey: 'FIRST_NAME', width: 100, formatter: myBoldHtmlFormatter, exportWithFormatter: false }, + { id: 'lastName', field: 'lastName', nameKey: 'LAST_NAME', width: 100, formatter: myBoldHtmlFormatter, exportCustomFormatter: myUppercaseFormatter, sanitizeDataExport: true }, + { id: 'position', field: 'position', name: 'Position', width: 100, formatter: Formatters.translate }, { id: 'order', field: 'order', width: 100, }, ] as Column[]; @@ -1460,7 +1292,7 @@ describe('ExcelExportService', () => { it(`should export same colspan in the export excel as defined in the grid`, async () => { mockCollection = [ - { id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, + { id: 0, userId: '1E06', firstName: 'John', lastName: 'X', position: 'SALES_REP', order: 10 }, { id: 1, userId: '1E09', firstName: 'Jane', lastName: 'Doe', position: 'DEVELOPER', order: 15 }, { id: 2, userId: '2ABC', firstName: 'Sponge', lastName: 'Bob', position: 'IT_ADMIN', order: 33 }, ]; diff --git a/packages/excel-export/src/excelExport.service.ts b/packages/excel-export/src/excelExport.service.ts index 157988d27..9bff88241 100644 --- a/packages/excel-export/src/excelExport.service.ts +++ b/packages/excel-export/src/excelExport.service.ts @@ -1,5 +1,4 @@ import * as ExcelBuilder from 'excel-builder-webpacker'; - import { // utility functions exportWithFormatterWhenDefined, @@ -11,7 +10,11 @@ import { Constants, ContainerService, ExcelExportService as BaseExcelExportService, + ExcelExportOption, ExternalResource, + ExcelWorkbook, + ExcelWorksheet, + FieldType, FileType, GridOption, KeyTitlePair, @@ -23,16 +26,12 @@ import { } from '@slickgrid-universal/common'; import { addWhiteSpaces, deepCopy, titleCase } from '@slickgrid-universal/utils'; - import { ExcelCellFormat, - ExcelExportOption, ExcelMetadata, ExcelStylesheet, - ExcelWorkbook, - ExcelWorksheet, } from './interfaces/index'; -import { useCellFormatByFieldType } from './excelUtils'; +import { getGroupTotalValue, getExcelFormatFromGridFormatter, useCellFormatByFieldType, ExcelFormatter, GetDataValueCallback } from './excelUtils'; const DEFAULT_EXPORT_OPTIONS: ExcelExportOption = { filename: 'export', @@ -55,11 +54,13 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ protected _translaterService: TranslaterService | undefined; protected _workbook!: ExcelWorkbook; + // references of each detected cell and/or group total formats + protected _regularCellExcelFormats: { [fieldId: string]: { stylesheetFormatterId?: number; getDataValueCallback: GetDataValueCallback; }; } = {}; + protected _groupTotalExcelFormats: { [fieldId: string]: { groupType: string; stylesheetFormatter?: ExcelFormatter; }; } = {}; + /** ExcelExportService class name which is use to find service instance in the external registered services */ readonly className = 'ExcelExportService'; - constructor() { } - protected get _datasetIdPropName(): string { return this._gridOptions?.datasetIdPropertyName ?? 'id'; } @@ -123,6 +124,10 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ this._excelExportOptions = deepCopy({ ...DEFAULT_EXPORT_OPTIONS, ...this._gridOptions.excelExportOptions, ...options }); this._fileFormat = this._excelExportOptions.format || FileType.xlsx; + // reset references of detected Excel formats + this._regularCellExcelFormats = {}; + this._groupTotalExcelFormats = {}; + // 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' }; @@ -430,7 +435,7 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ /** * Get all the grid row data and return that as an output string */ - protected pushAllGridRowDataToArray(originalDaraArray: Array, columns: Column[]): Array { + protected pushAllGridRowDataToArray(originalDaraArray: Array>, columns: Column[]): Array> { const lineCount = this._dataView.getLength(); // loop through all the grid rows of data @@ -472,6 +477,7 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ for (let col = 0; col < columnsLn; col++) { const columnDef = columns[col]; + const fieldType = columnDef.outputType || columnDef.type || FieldType.string; // skip excluded column if (columnDef.excludeFromExport) { @@ -527,20 +533,30 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ colspan = prevColspan--; } } else { + let itemData: Date | number | string | ExcelCellFormat = ''; + // -- Read Data & Push to Data Array - // 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, columnDef, itemObj, this._grid, this._excelExportOptions); + // user might want to export with Formatter, and/or auto-detect Excel format, and/or export as regular cell data + if (columnDef.exportWithFormatter || columnDef.exportCustomFormatter) { + // get the output by analyzing if we'll pull the value from the cell or from a formatter + itemData = exportWithFormatterWhenDefined(row, col, columnDef, itemObj, this._grid, this._excelExportOptions); + } else if (this.isExportingWithExcelFormat(columnDef)) { + // auto-detect best possible Excel format, we only do this check once per column (everything after that will be pull from temp ref) + if (!this._regularCellExcelFormats.hasOwnProperty(columnDef.id)) { + this._regularCellExcelFormats[columnDef.id] = useCellFormatByFieldType(this._stylesheet, this._stylesheetFormats, columnDef, this._grid); + } + const { stylesheetFormatterId, getDataValueCallback } = this._regularCellExcelFormats[columnDef.id]; + itemData = getDataValueCallback(itemObj[columnDef.field], stylesheetFormatterId, fieldType); + } else { + // at this point user might still have enabled exportWithFormatter in grid options or else cell will be exported "as is" + itemData = exportWithFormatterWhenDefined(row, col, columnDef, itemObj, this._grid, this._excelExportOptions); + } // does the user want to sanitize the output data (remove HTML tags)? - if (columnDef.sanitizeDataExport || this._excelExportOptions.sanitizeDataExport) { + if (!this.isExportingWithExcelFormat(columnDef) && typeof itemData === 'string' && (columnDef.sanitizeDataExport || this._excelExportOptions.sanitizeDataExport)) { itemData = sanitizeHtmlToText(itemData as string); } - // use different Excel Stylesheet Format as per the Field Type - if (!columnDef.exportWithFormatter) { - itemData = useCellFormatByFieldType(this._stylesheet, this._stylesheetFormats, itemData as string, columnDef, this._grid); - } - rowOutputStrings.push(itemData); idx++; } @@ -549,6 +565,20 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ return rowOutputStrings as string[]; } + protected isExportingWithExcelFormat(columnDef: Column) { + let isExportWithExcelFormatEnabled = false; + + // first check if there are any export options provided (as Grid Options) + if (this._excelExportOptions?.hasOwnProperty('exportWithExcelFormat')) { + isExportWithExcelFormatEnabled = !!this._excelExportOptions.exportWithExcelFormat; + } + // second check if "exportWithExcelFormat" is provided in the column definition, if so it will have precendence over the Grid Options exportOptions + if (columnDef?.hasOwnProperty('exportWithExcelFormat')) { + isExportWithExcelFormatEnabled = !!columnDef.exportWithExcelFormat; + } + return isExportWithExcelFormatEnabled; + } + /** * 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 @@ -570,26 +600,42 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ * 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 */ - protected readGroupedTotalRows(columns: Column[], itemObj: any): string[] { + protected readGroupedTotalRows(columns: Column[], itemObj: any): Array { const groupingAggregatorRowText = this._excelExportOptions.groupingAggregatorRowText || ''; - const outputStrings = [groupingAggregatorRowText]; + const outputStrings: Array = [groupingAggregatorRowText]; columns.forEach((columnDef) => { - let itemData = ''; - + let itemData: number | string | ExcelCellFormat = ''; + const fieldType = columnDef.outputType || columnDef.type || FieldType.string; 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, this._grid); + // use different Excel Stylesheet Format as per the Field Type + if (this.isExportingWithExcelFormat(columnDef) && fieldType === FieldType.number) { + let groupCellFormat = this._groupTotalExcelFormats[columnDef.id]; + if (!groupCellFormat?.groupType) { + groupCellFormat = getExcelFormatFromGridFormatter(this._stylesheet, this._stylesheetFormats, columnDef, this._grid, 'group'); + this._groupTotalExcelFormats[columnDef.id] = groupCellFormat; + } + + if (itemObj[groupCellFormat.groupType]?.[columnDef.field] !== undefined) { + itemData = { + value: getGroupTotalValue(itemObj, groupCellFormat.groupType, columnDef.field), + metadata: { style: groupCellFormat.stylesheetFormatter?.id } + }; + } } else { - if (columnDef.groupTotalsFormatter) { - itemData = columnDef.groupTotalsFormatter(itemObj, columnDef, this._grid); + // 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, this._grid); + } else { + if (columnDef.groupTotalsFormatter) { + itemData = columnDef.groupTotalsFormatter(itemObj, columnDef, this._grid); + } } } // does the user want to sanitize the output data (remove HTML tags)? - if (columnDef.sanitizeDataExport || this._excelExportOptions.sanitizeDataExport) { + if (typeof itemData === 'string' && (columnDef.sanitizeDataExport || this._excelExportOptions.sanitizeDataExport)) { itemData = sanitizeHtmlToText(itemData); } diff --git a/packages/excel-export/src/excelUtils.spec.ts b/packages/excel-export/src/excelUtils.spec.ts index b98758f93..eeff7432e 100644 --- a/packages/excel-export/src/excelUtils.spec.ts +++ b/packages/excel-export/src/excelUtils.spec.ts @@ -1,7 +1,6 @@ -import { Column, ExcelStylesheet, FieldType, Formatters, GridOption, SlickGrid } from '@slickgrid-universal/common'; -import moment from 'moment-mini'; +import { Column, ExcelStylesheet, FieldType, Formatters, GridOption, GroupTotalFormatters, SlickGrid } from '@slickgrid-universal/common'; -import { useCellFormatByFieldType } from './excelUtils'; +import { getExcelFormatFromGridFormatter, getNumericFormatterOptions, useCellFormatByFieldType } from './excelUtils'; const mockGridOptions = { enableExcelExport: true, @@ -39,209 +38,231 @@ describe('excelUtils', () => { it('should call createFormat with a format of an ISO date when FieldType.dateTime is provided', () => { const column = { type: FieldType.dateTime } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTime); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'YYYY-MM-DD HH:mm:ss' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2012-02-28 15:07:59' }); + expect(output).toEqual('2012-02-28 15:07:59'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeIso is provided', () => { const column = { type: FieldType.dateTimeIso } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeIso); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'YYYY-MM-DD HH:mm:ss' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2012-02-28 15:07:59' }); + expect(output).toEqual('2012-02-28 15:07:59'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeShortIso is provided', () => { const column = { type: FieldType.dateTimeShortIso } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeShortIso); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'YYYY-MM-DD HH:mm' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2012-02-28 15:07' }); + expect(output).toEqual('2012-02-28 15:07'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeIsoAmPm is provided', () => { const column = { type: FieldType.dateTimeIsoAmPm } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeIsoAmPm); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'YYYY-MM-DD hh:mm:ss a' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2012-02-28 03:07:59 pm' }); + expect(output).toEqual('2012-02-28 03:07:59 pm'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeIsoAM_PM is provided', () => { const column = { type: FieldType.dateTimeIsoAM_PM } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeIsoAM_PM); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'YYYY-MM-DD hh:mm:ss A' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2012-02-28 03:07:59 PM' }); + expect(output).toEqual('2012-02-28 03:07:59 PM'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateEuro is provided', () => { const column = { type: FieldType.dateEuro } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateEuro); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'DD/MM/YYYY' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '28/02/2012' }); + expect(output).toEqual('28/02/2012'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateEuroShort is provided', () => { const column = { type: FieldType.dateEuroShort } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateEuroShort); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'D/M/YY' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '28/2/12' }); + expect(output).toEqual('28/2/12'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeEuro is provided', () => { const column = { type: FieldType.dateTimeEuro } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeEuro); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'DD/MM/YYYY HH:mm:ss' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '28/02/2012 15:07:59' }); + expect(output).toEqual('28/02/2012 15:07:59'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeShortEuro is provided', () => { const column = { type: FieldType.dateTimeShortEuro } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeShortEuro); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'DD/MM/YYYY HH:mm' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '28/02/2012 15:07' }); + expect(output).toEqual('28/02/2012 15:07'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeEuroAmPm is provided', () => { const column = { type: FieldType.dateTimeEuroAmPm } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeEuroAmPm); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'DD/MM/YYYY hh:mm:ss a' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '28/02/2012 03:07:59 pm' }); + expect(output).toEqual('28/02/2012 03:07:59 pm'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeEuroAM_PM is provided', () => { const column = { type: FieldType.dateTimeEuroAM_PM } as Column; - const input = '2012-02-28 15:07:59'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:59'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeEuroAM_PM); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'DD/MM/YYYY hh:mm:ss A' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '28/02/2012 03:07:59 PM' }); + expect(output).toEqual('28/02/2012 03:07:59 PM'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeEuroShort is provided', () => { const column = { type: FieldType.dateTimeEuroShort } as Column; - const input = '2012-02-28 15:07:46'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:46'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeEuroShort); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'D/M/YY H:m:s' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '28/2/12 15:7:46' }); + expect(output).toEqual('28/2/12 15:7:46'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeEuroShortAmPm is provided', () => { const column = { type: FieldType.dateTimeEuroShortAmPm } as Column; - const input = '2012-02-28 15:07:46'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const input = new Date('2012-02-28 15:07:46'); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeEuroShortAmPm); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'D/M/YY h:m:s a' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '28/2/12 3:7:46 pm' }); + expect(output).toEqual('28/2/12 3:7:46 pm'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateUs is provided', () => { const column = { type: FieldType.dateUs } as Column; const input = new Date('2012-02-28 15:07:59'); - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateUs); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'MM/DD/YYYY' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '02/28/2012' }); + expect(output).toEqual('02/28/2012'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateUsShort is provided', () => { const column = { type: FieldType.dateUsShort } as Column; const input = new Date('2012-02-28 15:07:59'); - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateUsShort); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'M/D/YY' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2/28/12' }); + expect(output).toEqual('2/28/12'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeUs is provided', () => { const column = { type: FieldType.dateTimeUs } as Column; const input = new Date('2012-02-28 15:07:59'); - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeUs); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'MM/DD/YYYY HH:mm:ss' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '02/28/2012 15:07:59' }); + expect(output).toEqual('02/28/2012 15:07:59'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeShortUs is provided', () => { const column = { type: FieldType.dateTimeShortUs } as Column; const input = new Date('2012-02-28 15:07:59'); - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeShortUs); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'MM/DD/YYYY HH:mm' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '02/28/2012 15:07' }); + expect(output).toEqual('02/28/2012 15:07'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeUsAmPm is provided', () => { const column = { type: FieldType.dateTimeUsAmPm } as Column; const input = new Date('2012-02-28 15:07:59'); - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeUsAmPm); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'MM/DD/YYYY hh:mm:ss a' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '02/28/2012 03:07:59 pm' }); + expect(output).toEqual('02/28/2012 03:07:59 pm'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeUsAM_PM is provided', () => { const column = { type: FieldType.dateTimeUsAM_PM } as Column; const input = new Date('2012-02-28 15:07:59'); - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeUsAM_PM); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'MM/DD/YYYY hh:mm:ss A' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '02/28/2012 03:07:59 PM' }); + expect(output).toEqual('02/28/2012 03:07:59 PM'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeUsShort is provided', () => { const column = { type: FieldType.dateTimeUsShort } as Column; const input = new Date('2012-02-28 15:07:59'); - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeUsShort); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'M/D/YY H:m:s' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2/28/12 15:7:59' }); + expect(output).toEqual('2/28/12 15:7:59'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); it('should call createFormat with a format of an ISO date when FieldType.dateTimeUsShortAmPm is provided', () => { const column = { type: FieldType.dateTimeUsShortAmPm } as Column; const input = new Date('2012-02-28 15:07:59'); - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateTimeUsShortAmPm); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'M/D/YY h:m:s a' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2/28/12 3:7:59 pm' }); + expect(output).toEqual('2/28/12 3:7:59 pm'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); - xit('should call createFormat with a format of an ISO date when FieldType.dateUtc is provided', () => { - const column = { type: FieldType.dateUtc } as Column; - const input = moment('2013-05-23T17:55:00.325').utcOffset(420); // timezone that is +7 UTC hours - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + // xit('should call createFormat with a format of an ISO date when FieldType.dateUtc is provided', () => { + // const column = { type: FieldType.dateUtc } as Column; + // const input = moment('2013-05-23T17:55:00.325').utcOffset(420); // timezone that is +7 UTC hours + // const output = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2013-05-24T04:55:00.325+07:00' }); - }); + // expect(createFormatSpy).toHaveBeenCalledWith({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }); + // expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2013-05-24T04:55:00.325+07:00' }); + // }); it('should call createFormat with a format of an ISO date when FieldType.dateIso is provided', () => { const column = { type: FieldType.dateIso } as Column; const input = '2012-02-28 15:07:46'; - const output = useCellFormatByFieldType(stylesheetStub, {}, input, column, gridStub); + const result = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); + const output = result.getDataValueCallback(input, undefined, FieldType.dateIso); - expect(createFormatSpy).toHaveBeenCalledWith({ format: 'YYYY-MM-DD' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: '2012-02-28' }); + expect(output).toEqual('2012-02-28'); + expect(result).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: undefined }); }); }); @@ -252,51 +273,605 @@ describe('excelUtils', () => { it('should call createFormat with a format of "###0.00" when a number is provided without any specific formatter options', () => { const column = { type: FieldType.number, formatter: Formatters.decimal } as Column; - const output = useCellFormatByFieldType(stylesheetStub, {}, '12', column, gridStub); + const output = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); - expect(createFormatSpy).toHaveBeenCalledWith({ format: '###0.00' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: 12 }); + expect(createFormatSpy).toHaveBeenCalledWith({ format: '0.00;"-"0.00' }); + expect(output).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: 135 }); }); - it('should call createFormat with a format of "###0.0##" when a number is provided minDecimal & maxDecimal formatter options', () => { + it('should call createFormat with a format of "0.0##" when a number is provided minDecimal & maxDecimal formatter options', () => { const column = { type: FieldType.number, formatter: Formatters.decimal, params: { minDecimal: 1, maxDecimal: 3 } } as Column; - const output = useCellFormatByFieldType(stylesheetStub, {}, '12', column, gridStub); + const output = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); - expect(createFormatSpy).toHaveBeenCalledWith({ format: '###0.0##' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: 12 }); + expect(createFormatSpy).toHaveBeenCalledWith({ format: '0.0##;"-"0.0##' }); + expect(output).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: 135 }); }); - it('should call createFormat with a format of "€ ###0.00" when a number is provided minDecimal & maxDecimal formatter options', () => { - const column = { type: FieldType.number, formatter: Formatters.decimal, params: { numberPrefix: '€ ' } } as Column; - const output = useCellFormatByFieldType(stylesheetStub, {}, '12', column, gridStub); + it('should call createFormat with a format of "€0.00" when a number is provided minDecimal & maxDecimal formatter options', () => { + const column = { type: FieldType.number, formatter: Formatters.decimal, params: { numberPrefix: '€' } } as Column; + const output = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); - expect(createFormatSpy).toHaveBeenCalledWith({ format: '€ ###0.00' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: 12 }); + expect(createFormatSpy).toHaveBeenCalledWith({ format: '"€"0.00;"-€"0.00' }); + expect(output).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: 135 }); }); it('should call createFormat with a format of "#,##0.00" when a number is provided minDecimal & maxDecimal formatter options', () => { const column = { type: FieldType.number, formatter: Formatters.decimal, params: { thousandSeparator: ',' } } as Column; - const output = useCellFormatByFieldType(stylesheetStub, {}, '12', column, gridStub); + const output = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); - expect(createFormatSpy).toHaveBeenCalledWith({ format: '#,##0.00' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: 12 }); + expect(createFormatSpy).toHaveBeenCalledWith({ format: '#,##0.00;"-"#,##0.00' }); + expect(output).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: 135 }); }); - it('should call createFormat with a format of "# ##0.00 USD" when a number is provided thousandSeparator & numberSuffix formatter options', () => { + it('should call createFormat with a format of "# ##0.00 USD" when a number is provided with thousandSeparator & numberSuffix formatter options', () => { const column = { type: FieldType.number, formatter: Formatters.decimal, params: { thousandSeparator: ' ', numberSuffix: ' USD' } } as Column; - const output = useCellFormatByFieldType(stylesheetStub, {}, '12', column, gridStub); + const output = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); - expect(createFormatSpy).toHaveBeenCalledWith({ format: '# ##0.00 USD' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: 12 }); + expect(createFormatSpy).toHaveBeenCalledWith({ format: '# ##0.00" USD";"-"# ##0.00" USD"' }); + expect(output).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: 135 }); }); it('should call createFormat with a format of "#,##0.00 USD;(#,##0.00 USD)" when a number is provided displayNegativeNumberWithParentheses, thousandSeparator & numberSuffix formatter options', () => { const column = { type: FieldType.number, formatter: Formatters.decimal, params: { displayNegativeNumberWithParentheses: true, thousandSeparator: ',', numberSuffix: ' USD' } } as Column; - const output = useCellFormatByFieldType(stylesheetStub, {}, '12', column, gridStub); + const output = useCellFormatByFieldType(stylesheetStub, {}, column, gridStub); - expect(createFormatSpy).toHaveBeenCalledWith({ format: '#,##0.00 USD;(#,##0.00 USD)' }); - expect(output).toEqual({ metadata: { style: mockedFormatId }, value: 12 }); + expect(createFormatSpy).toHaveBeenCalledWith({ format: '#,##0.00" USD";(#,##0.00" USD")' }); + expect(output).toEqual({ getDataValueCallback: expect.toBeFunction(), stylesheetFormatterId: 135 }); }); }); + describe('getNumericFormatterOptions() method', () => { + describe('with GroupTotalFormatters', () => { + it('should get formatter options for GroupTotalFormatters.avgTotalsDollar', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.avgTotalsDollar, + params: { displayNegativeNumberWithParentheses: true, thousandSeparator: ',', numberSuffix: ' USD' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 4, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: ',', + wrapNegativeNumber: true, + }); + }); + + it('should get formatter options for GroupTotalFormatters.sumTotalsDollarColoredBold', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollarColoredBold, + params: { thousandSeparator: ' ', decimalSeparator: ',', numberSuffix: ' USD' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: ',', + maxDecimal: 4, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: ' ', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for GroupTotalFormatters.sumTotalsDollarColored', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollarColored, + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 4, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: '', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for GroupTotalFormatters.sumTotalsDollarBold', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollarBold, + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 4, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: '', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for GroupTotalFormatters.sumTotalsDollar', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollar, + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 4, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: '', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for GroupTotalFormatters.avgTotalsPercentage', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.avgTotalsPercentage, + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: undefined, + minDecimal: undefined, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: '', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for GroupTotalFormatters.avgTotals', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.avgTotals, + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 2, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: '', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for GroupTotalFormatters.minTotals', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.minTotals, + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 2, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: '', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for GroupTotalFormatters.maxTotals', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.maxTotals, + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 2, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: '', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for GroupTotalFormatters.sumTotalsColored', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsColored, + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 2, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: '', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for GroupTotalFormatters.sumTotals', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, + groupTotalsFormatter: GroupTotalFormatters.sumTotals, + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 2, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: '', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for GroupTotalFormatters.sumTotalsBold', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, + groupTotalsFormatter: GroupTotalFormatters.sumTotalsBold, + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'group'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 2, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: '', + wrapNegativeNumber: false, + }); + }); + }); + + describe('with regular Formatters', () => { + it('should get formatter options for Formatters.dollarColoredBold', () => { + const column = { + type: FieldType.number, formatter: Formatters.dollarColoredBold, + params: { displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'cell'); + + expect(output).toEqual({ + 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, + params: { displayNegativeNumberWithParentheses: false, thousandSeparator: ' ' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'cell'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 4, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: ' ', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for Formatters.dollar', () => { + const column = { + type: FieldType.number, formatter: Formatters.dollar, + params: { displayNegativeNumberWithParentheses: false, thousandSeparator: ' ' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'cell'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 4, + minDecimal: 2, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: ' ', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for Formatters.percent', () => { + const column = { + type: FieldType.number, formatter: Formatters.percent, + params: { displayNegativeNumberWithParentheses: false, thousandSeparator: ' ' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'cell'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: undefined, + minDecimal: undefined, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: ' ', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for Formatters.percentComplete', () => { + const column = { + type: FieldType.number, formatter: Formatters.percentComplete, + params: { displayNegativeNumberWithParentheses: false, thousandSeparator: ' ' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'cell'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: undefined, + minDecimal: undefined, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: ' ', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for Formatters.percentSymbol', () => { + const column = { + type: FieldType.number, formatter: Formatters.percentSymbol, + params: { displayNegativeNumberWithParentheses: false, thousandSeparator: ' ' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'cell'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: undefined, + minDecimal: undefined, + numberPrefix: '', + numberSuffix: '', + thousandSeparator: ' ', + wrapNegativeNumber: false, + }); + }); + + it('should get formatter options for Formatters.decimal', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, + params: { displayNegativeNumberWithParentheses: false, thousandSeparator: ' ', numberPrefix: 'Dollar ', numberSuffix: ' USD' } + } as Column; + const output = getNumericFormatterOptions(column, gridStub, 'cell'); + + expect(output).toEqual({ + decimalSeparator: '.', + maxDecimal: 2, + minDecimal: 2, + numberPrefix: 'Dollar ', + numberSuffix: ' USD', + thousandSeparator: ' ', + wrapNegativeNumber: false, + }); + }); + }); + }); + + describe('getExcelFormatFromGridFormatter() method', () => { + describe('with GroupTotalFormatters', () => { + it('should get excel excel metadata style format for GroupTotalFormatters.avgTotalsPercentage', () => { + const column = { + type: FieldType.number, + formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.avgTotalsPercentage, + params: { displayNegativeNumberWithParentheses: true, thousandSeparator: ',', numberSuffix: ' USD' } + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'avg', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.avgTotalsDollar', () => { + const column = { + type: FieldType.number, + formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.avgTotalsDollar, + params: { thousandSeparator: ' ', decimalSeparator: ',', numberSuffix: ' USD' } + } as Column; + + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'avg', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.sumTotalsDollarColored', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.avgTotals, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'avg', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.minTotals', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.minTotals, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'min', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.maxTotals', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.maxTotals, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'max', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.sumTotalsColored', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsColored, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'sum', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.sumTotalsDollarColoredBold', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollarColoredBold, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'sum', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.sumTotalsDollarColored', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollarColored, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'sum', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.sumTotalsDollarBold', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollarBold, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'sum', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.sumTotalsDollar', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollar, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'sum', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.sumTotals', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotals, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'sum', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style format for GroupTotalFormatters.sumTotalsBold', () => { + const column = { + type: FieldType.number, formatter: Formatters.decimal, groupTotalsFormatter: GroupTotalFormatters.sumTotalsBold, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'group'); + + expect(output).toEqual({ groupType: 'sum', stylesheetFormatter: { id: 135 } }); + }); + + 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}`, + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, { numberFormatter: { id: 3 } }, columnDef, gridStub, 'group'); + + expect(output).toEqual({ groupType: '', stylesheetFormatter: { id: 3 } }); + }); + }); + + describe('with regular Formatters', () => { + it('should get excel excel metadata style format for Formatters.dollarColoredBold', () => { + const column = { + type: FieldType.number, formatter: Formatters.dollarColoredBold, + params: { displayNegativeNumberWithParentheses: true, 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.dollarColored', () => { + const column = { + type: FieldType.number, + formatter: Formatters.dollarColored, + params: { 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', () => { + const column = { + type: FieldType.number, + formatter: Formatters.dollar, + params: { 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.percent', () => { + const column = { + type: FieldType.number, + formatter: Formatters.percent, + params: { 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.percentComplete', () => { + const column = { + type: FieldType.number, + formatter: Formatters.percentComplete, + params: { 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.percentSymbol', () => { + const column = { + type: FieldType.number, + formatter: Formatters.percentSymbol, + params: { 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.decimal', () => { + const column = { + type: FieldType.number, + formatter: Formatters.decimal, + params: { displayNegativeNumberWithParentheses: false, thousandSeparator: ' ', numberPrefix: 'Dollar ', numberSuffix: ' USD' } + } as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, {}, column, gridStub, 'cell'); + + expect(output).toEqual({ groupType: '', stylesheetFormatter: { id: 135 } }); + }); + + it('should get excel excel metadata style with regular number format when a custom Formatter is provided', () => { + const columnDef = { + type: FieldType.number, + formatter: () => `Something rendered`, + } as unknown as Column; + const output = getExcelFormatFromGridFormatter(stylesheetStub, { numberFormatter: { id: 3 } }, columnDef, gridStub, 'cell'); + + expect(output).toEqual({ groupType: '', stylesheetFormatter: { id: 3 } }); + }); + }); + }); }); \ No newline at end of file diff --git a/packages/excel-export/src/excelUtils.ts b/packages/excel-export/src/excelUtils.ts index 2156a1338..eaa7a072d 100644 --- a/packages/excel-export/src/excelUtils.ts +++ b/packages/excel-export/src/excelUtils.ts @@ -1,11 +1,45 @@ -import { Column, ExcelCellFormat, ExcelStylesheet, FieldType, Formatters, getValueFromParamsOrFormatterOptions, mapMomentDateFormatWithFieldType, SlickGrid } from '@slickgrid-universal/common'; +import { + Column, + ExcelCellFormat, + ExcelStylesheet, + FieldType, + Formatters, + FormatterType, + GroupTotalFormatters, + isNumber, + mapMomentDateFormatWithFieldType, + retrieveFormatterOptions, + sanitizeHtmlToText, + SlickGrid, +} from '@slickgrid-universal/common'; import * as moment_ from 'moment-mini'; const moment = (moment_ as any)['default'] || moment_; // patch to fix rollup "moment has no default export" issue, document here https://github.com/rollup/rollup/issues/670 +export type ExcelFormatter = object & { id: number; }; +export type GetDataValueCallback = (data: Date | string | number, excelFormatterId: number | undefined, fieldType: typeof FieldType[keyof typeof FieldType]) => Date | string | number | ExcelCellFormat; + +// define all type of potential excel data function callbacks +const getExcelInputDataCallback: GetDataValueCallback = (data) => data; +const getExcelNumberCallback: GetDataValueCallback = (data, excelFormatterId) => ({ + value: isNumber(data) ? +data : data, + metadata: { style: excelFormatterId } +}); +const getExcelDateCallback: GetDataValueCallback = (data, _excelFormatterId, fieldType) => { + let outputData: any; + if (data) { + const dateFormat: string = mapMomentDateFormatWithFieldType(fieldType); + const outputDate: moment_.Moment = moment(data, dateFormat, false); + if (outputDate.isValid()) { + outputData = outputDate.format(dateFormat); + } + } + return outputData ?? data; +}; + /** use different Excel Stylesheet Format as per the Field Type */ -export function useCellFormatByFieldType(stylesheet: ExcelStylesheet, stylesheetFormatters: any, data: string | Date | moment_.Moment, columnDef: Column, grid: SlickGrid): ExcelCellFormat | string { +export function useCellFormatByFieldType(stylesheet: ExcelStylesheet, stylesheetFormatters: any, columnDef: Column, grid: SlickGrid) { const fieldType = columnDef.outputType || columnDef.type || FieldType.string; - let outputData: ExcelCellFormat | string | Date | moment_.Moment = data; + let stylesheetFormatterId: number | undefined; switch (fieldType) { case FieldType.dateTime: @@ -32,61 +66,202 @@ export function useCellFormatByFieldType(stylesheet: ExcelStylesheet, stylesheet 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; - if (!stylesheetFormatters.hasOwnProperty(fieldType)) { - stylesheetFormatters[fieldType] = stylesheet.createFormat({ format: defaultDateFormat }); - } - outputData = { value: outputDate, metadata: { style: stylesheetFormatters[fieldType].id } }; - } - break; + return { stylesheetFormatterId: undefined, getDataValueCallback: getExcelDateCallback }; case FieldType.number: - const num = parseFloat(data as string); - const val = isNaN(num) ? null : +num; - let stylesheetFormatter: object & { id: string; }; - - switch (columnDef.formatter) { - case Formatters.decimal: - case Formatters.dollar: - case Formatters.dollarColored: - case Formatters.dollarColoredBold: - stylesheetFormatter = createOrGetStylesheetFormatter(stylesheet, stylesheetFormatters, columnDef, grid); - break; - default: - stylesheetFormatter = stylesheetFormatters.numberFormatter; - break; - } - outputData = { value: val, metadata: { style: stylesheetFormatter.id } }; - break; + stylesheetFormatterId = getExcelFormatFromGridFormatter(stylesheet, stylesheetFormatters, columnDef, grid, 'cell').stylesheetFormatter.id; + return { stylesheetFormatterId, getDataValueCallback: getExcelNumberCallback }; default: - outputData = data; + stylesheetFormatterId = undefined; + break; + } + return { stylesheetFormatterId, getDataValueCallback: getExcelInputDataCallback }; +} + +export function getGroupTotalValue(totals: any, groupType: string, colField: string) { + return totals?.[groupType]?.[colField] ?? 0; +} + +/** Get numeric formatter options when defined or use default values (minDecimal, maxDecimal, thousandSeparator, decimalSeparator, wrapNegativeNumber) */ +export function getNumericFormatterOptions(columnDef: Column, grid: SlickGrid, formatterType: FormatterType) { + let dataType: 'decimal' | 'dollar' | 'percent' | 'regular'; + + if (formatterType === 'group') { + switch (columnDef.groupTotalsFormatter) { + case GroupTotalFormatters.avgTotalsDollar: + case GroupTotalFormatters.sumTotalsDollarColoredBold: + case GroupTotalFormatters.sumTotalsDollarColored: + case GroupTotalFormatters.sumTotalsDollarBold: + case GroupTotalFormatters.sumTotalsDollar: + dataType = 'dollar'; + break; + case GroupTotalFormatters.avgTotalsPercentage: + dataType = 'percent'; + break; + case GroupTotalFormatters.avgTotals: + case GroupTotalFormatters.minTotals: + case GroupTotalFormatters.maxTotals: + case GroupTotalFormatters.sumTotalsColored: + case GroupTotalFormatters.sumTotals: + case GroupTotalFormatters.sumTotalsBold: + default: + // side note, formatters are using "regular" without any decimal limits (min, max), + // however in Excel export with custom format that doesn't work so well, we should use "decimal" to at least show optional decimals with "##" + dataType = 'decimal'; + break; + } + } else { + switch (columnDef.formatter) { + case Formatters.dollarColoredBold: + case Formatters.dollarColored: + case Formatters.dollar: + dataType = 'dollar'; + break; + case Formatters.percent: + case Formatters.percentCompleteBar: + case Formatters.percentCompleteBarWithText: + case Formatters.percentComplete: + 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 outputData as string; + return retrieveFormatterOptions(columnDef, grid, dataType, formatterType); } -function createOrGetStylesheetFormatter(stylesheet: ExcelStylesheet, stylesheetFormatters: any, columnDef: Column, grid: SlickGrid) { - const minDecimal = getValueFromParamsOrFormatterOptions('minDecimal', columnDef, grid, 2); - const maxDecimal = getValueFromParamsOrFormatterOptions('maxDecimal', columnDef, grid, 2); - const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, '.'); - const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, ''); - const numberPrefix = getValueFromParamsOrFormatterOptions('numberPrefix', columnDef, grid, ''); - const numberSuffix = getValueFromParamsOrFormatterOptions('numberSuffix', columnDef, grid, ''); - const displayNegativeNumberWithParentheses = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, false); - const numberFormat = `${numberPrefix}#${thousandSeparator}##0${decimalSeparator}${excelNumberFormatPadding(minDecimal, maxDecimal)}${numberSuffix}`; - const format = displayNegativeNumberWithParentheses ? `${numberFormat};(${numberFormat})` : numberFormat; - - if (!stylesheetFormatters.hasOwnProperty(format)) { - stylesheetFormatters[format] = stylesheet.createFormat({ format }); // save new formatter with its format as a prop key +export function getExcelFormatFromGridFormatter(stylesheet: ExcelStylesheet, stylesheetFormatters: any, columnDef: Column, grid: SlickGrid, formatterType: FormatterType) { + let format = ''; + let groupType = ''; + let stylesheetFormatter: undefined | ExcelFormatter; + const fieldType = columnDef.outputType || columnDef.type || FieldType.string; + + if (formatterType === 'group') { + switch (columnDef.groupTotalsFormatter) { + case GroupTotalFormatters.avgTotals: + case GroupTotalFormatters.avgTotalsDollar: + case GroupTotalFormatters.avgTotalsPercentage: + groupType = 'avg'; + break; + case GroupTotalFormatters.minTotals: + groupType = 'min'; + break; + case GroupTotalFormatters.maxTotals: + groupType = 'max'; + break; + case GroupTotalFormatters.sumTotals: + case GroupTotalFormatters.sumTotalsBold: + case GroupTotalFormatters.sumTotalsColored: + case GroupTotalFormatters.sumTotalsDollar: + case GroupTotalFormatters.sumTotalsDollarColoredBold: + case GroupTotalFormatters.sumTotalsDollarColored: + case GroupTotalFormatters.sumTotalsDollarBold: + groupType = 'sum'; + break; + default: + stylesheetFormatter = stylesheetFormatters.numberFormatter; + break; + } + } else { + switch (fieldType) { + case FieldType.number: + switch (columnDef.formatter) { + case Formatters.dollarColoredBold: + case Formatters.dollarColored: + case Formatters.dollar: + case Formatters.percent: + case Formatters.percentComplete: + case Formatters.percentCompleteBar: + case Formatters.percentCompleteBarWithText: + case Formatters.percentSymbol: + case Formatters.decimal: + format = createExcelFormatFromGridFormatter(columnDef, grid, 'cell'); + break; + default: + stylesheetFormatter = stylesheetFormatters.numberFormatter; + break; + } + break; + } } - return stylesheetFormatters[format]; + + if (!stylesheetFormatter && (columnDef.formatter || columnDef.groupTotalsFormatter)) { + format = createExcelFormatFromGridFormatter(columnDef, grid, formatterType, groupType); + if (!stylesheetFormatters.hasOwnProperty(format)) { + stylesheetFormatters[format] = stylesheet.createFormat({ format }); // save new formatter with its format as a prop key + } + stylesheetFormatter = stylesheetFormatters[format] as ExcelFormatter; + } + return { stylesheetFormatter: stylesheetFormatter as ExcelFormatter, groupType }; +} + +// -- +// private functions +// ------------------ + +function createFormatFromNumber(formattedVal: string) { + // full number syntax can have up to 7 sections, for example:: + // Total: ($10,420.55 USD) Expensed + const [ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _, + prefix, + openBraquet, + symbolPrefix, + number, + symbolSuffix, + closingBraquet, + suffix + ] = formattedVal?.match(/^([^\d\(\-]*)([\(]?)([^\d]*)([\-]?[\w]]?[\d\s]*[.,\d]*[\d]*[^)\s\%]?)([^\d.,)]*)([\)]?)([^\d]*)$/i) || []; + + // replace 1's by 0's (required numbers) and replace 2's by "#" (optional numbers) + const replacedNumber = (number || '').replace(/1/g, '0').replace(/[2]/g, '#'); + + // console.log('createFormatFromNumber', formattedVal.trim(), '|prefix:', prefix ?? '', '|openBraquet:', openBraquet ?? '', '|symbolPrefix:', symbolPrefix ?? '', '|input:', replacedNumber, '|symbolSuffix:', symbolSuffix ?? '', '|closingBraquet:', closingBraquet ?? '', '|suffix:', suffix ?? ''); + + const format = [ + escapeQuotes(prefix ?? ''), + openBraquet ?? '', + escapeQuotes(symbolPrefix ?? ''), + replacedNumber, + escapeQuotes(symbolSuffix ?? ''), + closingBraquet ?? '', + escapeQuotes(suffix ?? '') + ].join(''); + return format.replace(',', '\,'); +} + +function createExcelFormatFromGridFormatter(columnDef: Column, grid: SlickGrid, formatterType: FormatterType, groupType = '') { + let outputFormat = ''; + let positiveFormat = ''; + let negativeFormat = ''; + const { minDecimal, maxDecimal, thousandSeparator } = getNumericFormatterOptions(columnDef, grid, formatterType); + const leftInteger = thousandSeparator ? '2220' : '0'; + const testingNo = parseFloat(`${leftInteger}.${excelTestingDecimalNumberPadding(minDecimal, maxDecimal)}`); + + if (formatterType === 'group' && columnDef.groupTotalsFormatter) { + positiveFormat = sanitizeHtmlToText(columnDef.groupTotalsFormatter({ [groupType]: { [columnDef.field]: testingNo } }, columnDef, grid)); + negativeFormat = sanitizeHtmlToText(columnDef.groupTotalsFormatter({ [groupType]: { [columnDef.field]: -testingNo } }, columnDef, grid)); + } else if (columnDef.formatter) { + positiveFormat = sanitizeHtmlToText(columnDef.formatter(0, 0, testingNo, columnDef, {}, grid) as string); + negativeFormat = sanitizeHtmlToText(columnDef.formatter(0, 0, -testingNo, columnDef, {}, grid) as string); + } + if (positiveFormat && negativeFormat) { + outputFormat = createFormatFromNumber(positiveFormat) + ';' + createFormatFromNumber(negativeFormat); + } + return outputFormat; +} + +function escapeQuotes(val: string) { + return val ? `"${val}"` : val; } /** Get number format for a number cell, for example { minDecimal: 2, maxDecimal: 5 } will return "00###" */ -function excelNumberFormatPadding(minDecimal: number, maxDecimal: number) { - return textPadding('0', minDecimal) + textPadding('#', maxDecimal - minDecimal); +function excelTestingDecimalNumberPadding(minDecimal: number, maxDecimal: number) { + return textPadding('1', minDecimal) + textPadding('2', maxDecimal - minDecimal); } function textPadding(numberStr: string, count: number): string { diff --git a/packages/excel-export/src/interfaces/excelExportOption.interface.ts b/packages/excel-export/src/interfaces/excelExportOption.interface.ts deleted file mode 100644 index a23f1a5d3..000000000 --- a/packages/excel-export/src/interfaces/excelExportOption.interface.ts +++ /dev/null @@ -1,44 +0,0 @@ -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.xls | FileType.xlsx; - - /** 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 '⮞' or '\u25B7') */ - groupCollapsedSymbol?: string; - - /** Symbol use to show that the group title is expanded (you can use unicode like '⮟' 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/excelWorkbook.interface.ts b/packages/excel-export/src/interfaces/excelWorkbook.interface.ts deleted file mode 100644 index 58ad6bd53..000000000 --- a/packages/excel-export/src/interfaces/excelWorkbook.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index c7782e1d2..000000000 --- a/packages/excel-export/src/interfaces/excelWorksheet.interface.ts +++ /dev/null @@ -1,36 +0,0 @@ -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 index c18629845..839a812ea 100644 --- a/packages/excel-export/src/interfaces/index.ts +++ b/packages/excel-export/src/interfaces/index.ts @@ -1,6 +1,3 @@ export * from './excelCellFormat.interface'; -export * from './excelExportOption.interface'; export * from './excelMetadata.interface'; export * from './excelStylesheet.interface'; -export * from './excelWorkbook.interface'; -export * from './excelWorksheet.interface';