Skip to content

Commit

Permalink
feat(exports): add Excel custom cell (column) styling (#851)
Browse files Browse the repository at this point in the history
* feat(exports): add column custom Excel Styling
- add `excelExportOptions` and `groupTotalsExcelExportOptions` to allow for custom cell width & styling of every column.
  • Loading branch information
ghiscoding committed Dec 20, 2022
1 parent ad373ab commit dd92d44
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 63 deletions.
46 changes: 37 additions & 9 deletions examples/webpack-demo-vanilla-bundle/src/examples/example02.ts
Expand Up @@ -122,15 +122,34 @@ export class Example2 {
},
{
id: 'cost', name: 'Cost', field: 'cost',
minWidth: 70,
width: 80,
filterable: true,
minWidth: 70, width: 80,
sortable: true, filterable: true,
filter: { model: Filters.compoundInputNumber },
type: FieldType.number,
sortable: true,
formatter: Formatters.decimal,
groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollar,
params: { displayNegativeNumberWithParentheses: true, numberPrefix: '€ ', minDecimal: 2, maxDecimal: 4, groupFormatterPrefix: '<b>Total</b>: ' /* , groupFormatterSuffix: ' USD' */ },
formatter: Formatters.currency,
groupTotalsFormatter: GroupTotalFormatters.sumTotalsCurrency,
params: { displayNegativeNumberWithParentheses: true, currencyPrefix: '€', groupFormatterCurrencyPrefix: '€', minDecimal: 2, maxDecimal: 4, groupFormatterPrefix: '<b>Total</b>: ' },
excelExportOptions: {
style: {
font: { outline: true, italic: true },
format: '€0.00##;[Red](€0.00##)',
},
width: 18
},
groupTotalsExcelExportOptions: {
style: {
alignment: { horizontal: 'center' },
font: { bold: true, color: 'FF005289', underline: 'single', fontName: 'Consolas', size: 10 },
fill: { type: 'pattern', patternType: 'solid', fgColor: 'FFE6F2F6' },
border: {
top: { color: 'FFa500ff', style: 'thick', },
left: { color: 'FFa500ff', style: 'medium', },
right: { color: 'FFa500ff', style: 'dotted', },
bottom: { color: 'FFa500ff', style: 'double', },
},
format: '"Total: "€0.00##;[Red]"Total: "(€0.00##)'
},
},
},
{
id: 'effortDriven', name: 'Effort Driven',
Expand Down Expand Up @@ -167,7 +186,15 @@ export class Example2 {
onColumnsChanged: (e, args) => console.log(e, args)
},
enableExcelExport: true,
excelExportOptions: { filename: 'my-export', sanitizeDataExport: true, exportWithExcelFormat: true, },
excelExportOptions: {
filename: 'my-export',
sanitizeDataExport: true,
exportWithExcelFormat: true,
columnHeaderStyle: {
font: { color: 'FFFFFFFF' },
fill: { type: 'pattern', patternType: 'solid', fgColor: 'FF4a6c91' }
}
},
textExportOptions: { filename: 'my-export', sanitizeDataExport: true },
registerExternalResources: [this.excelExportService, new TextExportService()],
showCustomFooter: true, // display some metrics in the bottom custom footer
Expand All @@ -189,6 +216,7 @@ export class Example2 {
const randomMonth = Math.floor(Math.random() * 11);
const randomDay = Math.floor((Math.random() * 29));
const randomPercent = Math.round(Math.random() * 100);
const randomCost = (i % 33 === 0) ? null : Math.round(Math.random() * 10000) / 100;

tmpArray[i] = {
id: 'id_' + i,
Expand All @@ -199,7 +227,7 @@ export class Example2 {
percentCompleteNumber: randomPercent,
start: new Date(randomYear, randomMonth, randomDay),
finish: new Date(randomYear, (randomMonth + 1), randomDay),
cost: (i % 33 === 0) ? null : Math.round(Math.random() * 10000) / 100,
cost: i % 3 ? randomCost : -randomCost,
effortDriven: (i % 5 === 0)
};
}
Expand Down
15 changes: 11 additions & 4 deletions packages/common/src/interfaces/column.interface.ts
Expand Up @@ -2,6 +2,7 @@
import {
CellMenu,
ColumnEditor,
ColumnExcelExportOption,
ColumnFilter,
CustomTooltipOption,
EditorValidator,
Expand Down Expand Up @@ -79,6 +80,9 @@ export interface Column<T = any> {
/** Any inline editor function that implements Editor for the cell value or ColumnEditor */
editor?: ColumnEditor;

/** Excel export custom options for cell formatting & width, this option only works when `exportWithExcelFormat` is enabled */
excelExportOptions?: ColumnExcelExportOption;

/** Default to false, which leads to exclude the column title from the Column Picker. */
excludeFromColumnPicker?: boolean;

Expand All @@ -94,7 +98,7 @@ export interface Column<T = any> {
/** Defaults to false, which leads to exclude the column from getting a header menu. For example, the checkbox row selection should not have a header menu. */
excludeFromHeaderMenu?: boolean;

/** If defined this will be set as column width in Excel */
/** @deprecated @use `excelExportOptions` in the future. This option let you defined this Excel column width */
exportColumnWidth?: number;

/**
Expand All @@ -116,9 +120,9 @@ export interface Column<T = any> {
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.
* Defaults to true, which leads to ExcelExportService that will try to detect the best possible Excel format for each cell.
* The difference with the other flag is that "exportWithFormatter" will always export as a string, while this option here will try to detect the best Excel format and cell type.
* NOTE: Date will be exported as string (not as Excel Date), the numbers are the ones making the best out of this option.
*/
exportWithExcelFormat?: boolean;

Expand Down Expand Up @@ -163,6 +167,9 @@ export interface Column<T = any> {
/** Grouping option used by a Draggable Grouping Column */
grouping?: Grouping;

/** Excel export custom options for cell formatting & width, this option only works when `exportWithExcelFormat` is enabled */
groupTotalsExcelExportOptions?: Exclude<ColumnExcelExportOption, 'width'>;

/** Group Totals Formatter function that can be used to add grouping totals in the grid */
groupTotalsFormatter?: GroupTotalsFormatter;

Expand Down
@@ -0,0 +1,74 @@
/** Excel custom export options (formatting & width) that can be applied to a column */
export interface ColumnExcelExportOption {
/**
* Option to provide custom Excel styling
* NOTE: this option will completely override any detected column formatting
*/
style?: ExcelCustomStyling;

/** Excel column width */
width?: number;
}

/**
* Excel Color in ARGB format, for color aren't transparent just use "FF" as prefix.
* For example if the color you want to add is a blue with HTML color "#0000FF", then the excel color we need to add is "FF0000FF"
* Online tool: https://www.myfixguide.com/color-converter/
*/
export type ExcelColorStyle = string | { theme: number; };
export interface ExcelAlignmentStyle {
horizontal?: 'center' | 'fill' | 'general' | 'justify' | 'left' | 'right';
justifyLastLine?: boolean;
readingOrder?: string;
relativeIndent?: boolean;
shrinkToFit?: boolean;
textRotation?: string | number;
vertical?: 'bottom' | 'distributed' | 'center' | 'justify' | 'top';
wrapText?: boolean;
}
export type ExcelBorderLine = 'continuous' | 'dash' | 'dashDot' | 'dashDotDot' | 'dotted' | 'double' | 'lineStyleNone' | 'medium' | 'slantDashDot' | 'thin' | 'thick';
export interface ExcelBorderStyle {
bottom?: { color?: ExcelColorStyle; style?: ExcelBorderLine; };
top?: { color?: ExcelColorStyle; style?: ExcelBorderLine; };
left?: { color?: ExcelColorStyle; style?: ExcelBorderLine; };
right?: { color?: ExcelColorStyle; style?: ExcelBorderLine; };
diagonal?: any;
outline?: boolean;
diagonalUp?: boolean;
diagonalDown?: boolean;
}
export interface ExcelFillStyle {
type?: 'gradient' | 'pattern';
patternType?: string;
degree?: number;
fgColor?: ExcelColorStyle;
start?: ExcelColorStyle;
end?: { pureAt?: number; color?: ExcelColorStyle; };
}
export interface ExcelFontStyle {
bold?: boolean;
color?: ExcelColorStyle;
fontName?: string;
italic?: boolean;
outline?: boolean;
size?: number;
strike?: boolean;
subscript?: boolean;
superscript?: boolean;
underline?: 'single' | 'double' | 'singleAccounting' | 'doubleAccounting';
}

/** Excel custom formatting that will be applied to a column */
export interface ExcelCustomStyling {
alignment?: ExcelAlignmentStyle;
border?: ExcelBorderStyle;
fill?: ExcelFillStyle;
font?: ExcelFontStyle;
format?: string;
protection?: {
locked?: boolean;
hidden?: boolean;
};
/** style id */
style?: number;
}
11 changes: 6 additions & 5 deletions packages/common/src/interfaces/excelExportOption.interface.ts
@@ -1,3 +1,4 @@
import { ExcelCustomStyling } from './columnExcelExportOption.interface';
import { ExcelWorksheet } from './excelWorksheet.interface';
import { ExcelWorkbook } from './excelWorkbook.interface';
import { FileType } from '../enums/fileType.enum';
Expand All @@ -6,8 +7,8 @@ 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;
/** When defined, this will override header titles styling, when undefined the default will be a bold style */
columnHeaderStyle?: ExcelCustomStyling;

/** If set then this will be used as column width for all columns */
customColumnWidth?: number;
Expand All @@ -16,9 +17,9 @@ export interface ExcelExportOption {
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.
* Defaults to true, which leads to ExcelExportService that will try to detect the best possible Excel format for each cell.
* The difference with the other flag is that "exportWithFormatter" will always export as a string, while this option here will try to detect the best Excel format and cell type.
* NOTE: Date will be exported as string (not as Excel Date), the numbers are the ones making the best out of this option.
*/
exportWithExcelFormat?: boolean;

Expand Down
1 change: 1 addition & 0 deletions packages/common/src/interfaces/index.ts
Expand Up @@ -19,6 +19,7 @@ export * from './collectionSortBy.interface';
export * from './column.interface';
export * from './columnEditor.interface';
export * from './columnEditorDualInput.interface';
export * from './columnExcelExportOption.interface';
export * from './columnFilter.interface';
export * from './columnFilters.interface';
export * from './columnPicker.interface';
Expand Down
31 changes: 25 additions & 6 deletions packages/excel-export/src/excelExport.service.spec.ts
Expand Up @@ -20,7 +20,7 @@ import * as ExcelBuilder from 'excel-builder-webpacker';
import { ContainerServiceStub } from '../../../test/containerServiceStub';
import { TranslateServiceStub } from '../../../test/translateServiceStub';
import { ExcelExportService } from './excelExport.service';
import { useCellFormatByFieldType } from './excelUtils';
import { getExcelInputDataCallback, useCellFormatByFieldType } from './excelUtils';

const pubSubServiceStub = {
publish: jest.fn(),
Expand Down Expand Up @@ -542,14 +542,20 @@ describe('ExcelExportService', () => {
{ id: 'userId', field: 'userId', name: 'User Id', width: 100 },
{ 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: 'position', field: 'position', width: 100,
excelExportOptions: { style: { font: { outline: true, italic: true }, format: '€0.00##;[Red](€0.00##)' }, width: 18 }
},
{ id: 'startDate', field: 'startDate', type: FieldType.dateIso, width: 100, exportWithFormatter: false, },
{ id: 'endDate', field: 'endDate', width: 100, formatter: Formatters.dateIso, type: FieldType.dateUtc, exportWithFormatter: true, outputType: FieldType.dateIso },
] as Column[];

jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns);
});

afterEach(() => {
jest.clearAllMocks();
});

it(`should expect Date exported correctly when Field Type is provided and we use "exportWithFormatter" set to True & False`, async () => {
mockCollection = [
Expand All @@ -563,10 +569,10 @@ describe('ExcelExportService', () => {
const spyDownload = jest.spyOn(service, 'startDownloadFile');

const optionExpectation = { filename: 'export.xlsx', format: FileType.xlsx };

service.init(gridStub, container);
await service.exportToExcel(mockExportExcelOptions);

expect(service.stylesheet).toBeTruthy();
expect(pubSubSpy).toHaveBeenCalledWith(`onAfterExportToExcel`, optionExpectation);
expect(spyUrlCreate).toHaveBeenCalledWith(mockExcelBlob);
expect(spyDownload).toHaveBeenCalledWith({
Expand All @@ -583,6 +589,10 @@ describe('ExcelExportService', () => {
['1E09', 'Jane', 'Doe', 'HUMAN_RESOURCES', '2010-10-09', '2024-01-02'],
]
});
expect(service.regularCellExcelFormats.position).toEqual({
getDataValueCallback: getExcelInputDataCallback,
stylesheetFormatterId: 4,
});
});
});

Expand Down Expand Up @@ -828,6 +838,7 @@ describe('ExcelExportService', () => {
id: 'order', field: 'order', type: FieldType.number,
exportWithFormatter: true,
formatter: Formatters.multiple, params: { formatters: [myBoldHtmlFormatter, myCustomObjectFormatter] },
groupTotalsExcelExportOptions: { style: { font: { bold: true, italic: true }, format: '€0.00##;[Red](€0.00##)' }, },
groupTotalsFormatter: GroupTotalFormatters.sumTotals,
},
] as Column[];
Expand Down Expand Up @@ -892,11 +903,19 @@ describe('ExcelExportService', () => {
{ 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' } }],
['', '1E06', 'John', 'X', 'SALES_REP', { metadata: { style: 3, type: "number", }, value: 10, }],
['', '2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', { metadata: { style: 3, type: "number", }, value: 10, }],
['', '', '', '', '', { value: 20, metadata: { style: 5, type: 'number' } }],
]
});
expect(service.groupTotalExcelFormats.order).toEqual({
groupType: 'sum',
stylesheetFormatter: {
fontId: 2,
id: 5,
numFmtId: 103,
}
});
});
});

Expand Down

0 comments on commit dd92d44

Please sign in to comment.