Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(exports): add Excel custom cell (column) styling #851

Merged
merged 3 commits into from
Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions examples/webpack-demo-vanilla-bundle/src/examples/example02.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading