Skip to content

Commit

Permalink
fix: export to file/excel should also have tree indentation
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed May 5, 2021
1 parent 8b468f0 commit 8c4c2b8
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 26 deletions.
19 changes: 10 additions & 9 deletions examples/webpack-demo-vanilla-bundle/src/examples/example05.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,17 @@ export class Example5 {
this.columnDefinitions = [
{
id: 'title', name: 'Title', field: 'title', width: 220, cssClass: 'cell-title',
filterable: true, sortable: true,
filterable: true, sortable: true, exportWithFormatter: false,
queryFieldSorter: 'id', type: FieldType.string,
formatter: Formatters.tree,
formatter: Formatters.tree, exportCustomFormatter: Formatters.treeExport

},
{ id: 'duration', name: 'Duration', field: 'duration', minWidth: 90, filterable: true },
{
id: 'percentComplete', name: '% Complete', field: 'percentComplete', minWidth: 120, maxWidth: 200,
id: 'percentComplete', name: '% Complete', field: 'percentComplete',
minWidth: 120, maxWidth: 200, exportWithFormatter: false,
sortable: true, filterable: true, filter: { model: Filters.compoundSlider, operator: '>=' },
formatter: Formatters.percentCompleteBarWithText, type: FieldType.number,
formatter: Formatters.percentCompleteBar, type: FieldType.number,
},
{
id: 'start', name: 'Start', field: 'start', minWidth: 60,
Expand All @@ -62,7 +64,8 @@ export class Example5 {
},
{
id: 'effortDriven', name: 'Effort Driven', width: 80, minWidth: 20, maxWidth: 80, cssClass: 'cell-effort-driven', field: 'effortDriven',
formatter: Formatters.checkmarkMaterial, cannotTriggerInsert: true,
exportWithFormatter: false,
formatter: Formatters.checkmark, cannotTriggerInsert: true,
filterable: true,
filter: {
collection: [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }],
Expand All @@ -78,10 +81,8 @@ export class Example5 {
enableAutoSizeColumns: true,
enableAutoResize: true,
enableExcelExport: true,
excelExportOptions: {
exportWithFormatter: true,
sanitizeDataExport: true
},
exportOptions: { exportWithFormatter: true },
excelExportOptions: { exportWithFormatter: true },
registerExternalResources: [new ExcelExportService()],
enableFiltering: true,
showCustomFooter: true, // display some metrics in the bottom custom footer
Expand Down
132 changes: 132 additions & 0 deletions packages/common/src/formatters/__tests__/treeExportFormatter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Column, SlickDataView, GridOption, SlickGrid } from '../../interfaces/index';
import { treeExportFormatter } from '../treeExportFormatter';

const dataViewStub = {
getIdxById: jest.fn(),
getItemByIdx: jest.fn(),
getIdPropertyName: jest.fn(),
} as unknown as SlickDataView;

const gridStub = {
getData: jest.fn(),
getOptions: jest.fn(),
} as unknown as SlickGrid;

describe('Tree Export Formatter', () => {
let dataset: any[];
let mockGridOptions: GridOption;

beforeEach(() => {
dataset = [
{ id: 0, firstName: 'John', lastName: 'Smith', fullName: 'John Smith', email: '[email protected]', address: { zip: 123456 }, parentId: null, indent: 0 },
{ id: 1, firstName: 'Jane', lastName: 'Doe', fullName: 'Jane Doe', email: '[email protected]', address: { zip: 222222 }, parentId: 0, indent: 1 },
{ id: 2, firstName: 'Bob', lastName: 'Cane', fullName: 'Bob Cane', email: '[email protected]', address: { zip: 333333 }, parentId: 1, indent: 2, __collapsed: true },
{ id: 3, firstName: 'Barbara', lastName: 'Cane', fullName: 'Barbara Cane', email: '[email protected]', address: { zip: 444444 }, parentId: null, indent: 0, __collapsed: true },
{ id: 4, firstName: 'Anonymous', lastName: 'Doe', fullName: 'Anonymous < Doe', email: '[email protected]', address: { zip: 556666 }, parentId: null, indent: 0, __collapsed: true },
];
mockGridOptions = {
treeDataOptions: { levelPropName: 'indent' }
} as GridOption;
jest.spyOn(gridStub, 'getOptions').mockReturnValue(mockGridOptions);
});

it('should throw an error when oarams are mmissing', () => {
expect(() => treeExportFormatter(1, 1, 'blah', {} as Column, {}, gridStub))
.toThrowError('You must provide valid "treeDataOptions" in your Grid Options and it seems that there are no tree level found in this row');
});

it('should return empty string when DataView is not correctly formed', () => {
const output = treeExportFormatter(1, 1, '', {} as Column, dataset[1], gridStub);
expect(output).toBe('');
});

it('should return empty string when value is null', () => {
const output = treeExportFormatter(1, 1, null, {} as Column, dataset[1], gridStub);
expect(output).toBe('');
});

it('should return empty string when value is undefined', () => {
const output = treeExportFormatter(1, 1, undefined, {} as Column, dataset[1], gridStub);
expect(output).toBe('');
});

it('should return empty string when item is undefined', () => {
const output = treeExportFormatter(1, 1, 'blah', {} as Column, undefined, gridStub);
expect(output).toBe('');
});

it('should return a span without any icon and ', () => {
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[0]);

const output = treeExportFormatter(1, 1, dataset[0]['firstName'], {} as Column, dataset[0], gridStub);
expect(output).toBe(`John`);
});

it('should return a span without any icon and 15px indentation of a tree level 1', () => {
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

const output = treeExportFormatter(1, 1, dataset[1]['firstName'], {} as Column, dataset[1], gridStub);
expect(output).toBe(`. Jane`);
});

it('should return a span without any icon and 30px indentation of a tree level 2', () => {
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

const output = treeExportFormatter(1, 1, dataset[2]['firstName'], {} as Column, dataset[2], gridStub);
expect(output).toBe(`. Bob`);
});

it('should return a span with expanded icon and 15px indentation of a tree level 1 when current item is greater than next item', () => {
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[2]);

const output = treeExportFormatter(1, 1, dataset[1]['firstName'], {} as Column, dataset[1], gridStub);
expect(output).toBe(`⮟ Jane`);
});

it('should return a span with collapsed icon and 0px indentation of a tree level 0 when current item is lower than next item', () => {
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

const output = treeExportFormatter(1, 1, dataset[3]['firstName'], {} as Column, dataset[3], gridStub);
expect(output).toBe(`⮞ Barbara`);
});

it('should execute "queryFieldNameGetterFn" callback to get field name to use when it is defined', () => {
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

const mockColumn = { id: 'firstName', field: 'firstName', queryFieldNameGetterFn: (dataContext) => 'fullName' } as Column;
const output = treeExportFormatter(1, 1, null, mockColumn as Column, dataset[3], gridStub);
expect(output).toBe(`⮞ Barbara Cane`);
});

it('should execute "queryFieldNameGetterFn" callback to get field name and also apply html encoding when output value includes a character that should be encoded', () => {
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(2);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[2]);

const mockColumn = { id: 'firstName', field: 'firstName', queryFieldNameGetterFn: (dataContext) => 'fullName' } as Column;
const output = treeExportFormatter(1, 1, null, mockColumn as Column, dataset[4], gridStub);
expect(output).toBe(`⮞ Anonymous < Doe`);
});

it('should execute "queryFieldNameGetterFn" callback to get field name, which has (.) dot notation reprensenting complex object', () => {
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
jest.spyOn(dataViewStub, 'getIdxById').mockReturnValue(1);
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

const mockColumn = { id: 'zip', field: 'zip', queryFieldNameGetterFn: (dataContext) => 'address.zip' } as Column;
const output = treeExportFormatter(1, 1, null, mockColumn as Column, dataset[3], gridStub);
expect(output).toBe(`⮞ 444444`);
});
});
4 changes: 4 additions & 0 deletions packages/common/src/formatters/formatters.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { percentCompleteFormatter } from './percentCompleteFormatter';
import { percentSymbolFormatter } from './percentSymbolFormatter';
import { progressBarFormatter } from './progressBarFormatter';
import { translateFormatter } from './translateFormatter';
import { treeExportFormatter } from './treeExportFormatter';
import { treeFormatter } from './treeFormatter';
import { translateBooleanFormatter } from './translateBooleanFormatter';
import { uppercaseFormatter } from './uppercaseFormatter';
Expand Down Expand Up @@ -237,6 +238,9 @@ export const Formatters = {
/** Formatter that must be use with a Tree Data column */
tree: treeFormatter,

/** Formatter that must be use with a Tree Data column for Exporting the data */
treeExport: treeExportFormatter,

/** Takes a value and displays it all uppercase */
uppercase: uppercaseFormatter,

Expand Down
1 change: 1 addition & 0 deletions packages/common/src/formatters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export * from './percentSymbolFormatter';
export * from './progressBarFormatter';
export * from './translateFormatter';
export * from './translateBooleanFormatter';
export * from './treeExportFormatter';
export * from './treeFormatter';
export * from './uppercaseFormatter';
export * from './yesNoFormatter';
48 changes: 48 additions & 0 deletions packages/common/src/formatters/treeExportFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { SlickDataView, Formatter } from './../interfaces/index';
import { addWhiteSpaces, getDescendantProperty } from '../services/utilities';

/** Formatter that must be use with a Tree Data column */
export const treeExportFormatter: Formatter = (_row, _cell, value, columnDef, dataContext, grid) => {
const dataView = grid?.getData<SlickDataView>();
const gridOptions = grid?.getOptions();
const treeDataOptions = gridOptions?.treeDataOptions;
const treeLevelPropName = treeDataOptions?.levelPropName ?? '__treeLevel';
const indentMarginLeft = treeDataOptions?.exportIndentMarginLeft ?? 4;
const groupCollapsedSymbol = gridOptions?.excelExportOptions?.groupCollapsedSymbol ?? '⮞';
const groupExpandedSymbol = gridOptions?.excelExportOptions?.groupExpandedSymbol ?? '⮟';
let outputValue = value;

if (typeof columnDef.queryFieldNameGetterFn === 'function') {
const fieldName = columnDef.queryFieldNameGetterFn(dataContext);
if (fieldName?.indexOf('.') >= 0) {
outputValue = getDescendantProperty(dataContext, fieldName);
} else {
outputValue = dataContext.hasOwnProperty(fieldName) ? dataContext[fieldName] : value;
}
}
if (outputValue === null || outputValue === undefined || dataContext === undefined) {
return '';
}

if (!dataContext.hasOwnProperty(treeLevelPropName)) {
throw new Error('You must provide valid "treeDataOptions" in your Grid Options and it seems that there are no tree level found in this row');
}

if (dataView?.getItemByIdx) {
const identifierPropName = dataView.getIdPropertyName() || 'id';
const treeLevel = dataContext[treeLevelPropName] || 0;
const spacer = addWhiteSpaces(indentMarginLeft * treeLevel);
const idx = dataView.getIdxById(dataContext[identifierPropName]);
const nextItemRow = dataView.getItemByIdx((idx || 0) + 1);

if (nextItemRow?.[treeLevelPropName] > treeLevel) {
if (dataContext.__collapsed) {
return `${groupCollapsedSymbol} ${spacer} ${outputValue}`;
} else {
return `${groupExpandedSymbol} ${spacer} ${outputValue}`;
}
}
return treeLevel === 0 ? outputValue : `.${spacer} ${outputValue}`;
}
return '';
};
10 changes: 7 additions & 3 deletions packages/common/src/global-grid-options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DelimiterType, EventNamingStyle, FileType, GridAutosizeColsMode, OperatorType } from './enums/index';
import { Column, GridOption } from './interfaces/index';
import { Column, GridOption, TreeDataOption } from './interfaces/index';
import { Filters } from './filters';

/** Global Grid Options Defaults */
Expand Down Expand Up @@ -137,8 +137,8 @@ export const GlobalGridOptions: GridOption = {
filename: 'export',
format: FileType.xlsx,
groupingColumnHeaderTitle: 'Group By',
groupCollapsedSymbol: '\u25B9',
groupExpandedSymbol: '\u25BF',
groupCollapsedSymbol: '',
groupExpandedSymbol: '',
groupingAggregatorRowText: '',
sanitizeDataExport: false,
},
Expand Down Expand Up @@ -228,6 +228,10 @@ export const GlobalGridOptions: GridOption = {
resizeFormatterPaddingWidthInPx: 0,
resizeDefaultRatioForStringType: 0.88,
resizeMaxItemToInspectCellContentWidth: 1000,
treeDataOptions: {
exportIndentMarginLeft: 4,
exportIndentationLeadingChar: '.',
} as unknown as TreeDataOption
};

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/interfaces/excelExportOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ export interface ExcelExportOption {
/** The default text to display in 1st column of the File Export, which will identify that the current row is a Grouping Aggregator */
groupingAggregatorRowText?: string;

/** Symbol use to show that the group title is collapsed (you can use unicode like '\u25B9' or '\u25B7') */
/** 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 '\u25BF' or '\u25BD') */
/** 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. */
Expand Down
12 changes: 12 additions & 0 deletions packages/common/src/interfaces/treeDataOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,16 @@ export interface TreeDataOption {
* For example if tree depth level is 2, the calculation will be (2 * 15 = 30), so the column will be displayed 30px from the left
*/
indentMarginLeft?: number;

/**
* Defaults to 4, indentation spaces to add from the left (calculated by the tree level multiplied by this number).
* For example if tree depth level is 2, the calculation will be (2 * 15 = 30), so the column will be displayed 30px from the left
*/
exportIndentMarginLeft?: number;

/**
* Defaults to dot (.), we added this because Excel seems to trim spaces leading character
* and if we add a regular character like a dot then it keeps all tree level indentation spaces
*/
exportIndentationLeadingChar?: string;
}
16 changes: 8 additions & 8 deletions packages/excel-export/src/excelExport.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ describe('ExcelExportService', () => {
{ metadata: { style: 1, }, value: 'Position', },
{ metadata: { style: 1, }, value: 'Order', },
],
[' Order: 20 (2 items)'],
[' Order: 20 (2 items)'],
['', '1E06', 'John', 'Z', 'SALES_REP', '10'],
['', '2B02', 'Jane', 'DOE', 'FINANCE_MANAGER', '10'],
['', '', '', '', '', 'Custom: 20'],
Expand Down Expand Up @@ -818,7 +818,7 @@ describe('ExcelExportService', () => {
{ metadata: { style: 1, }, value: 'Position', },
{ metadata: { style: 1, }, value: 'Order', },
],
[' Order: 20 (2 items)'],
[' Order: 20 (2 items)'],
['', '1E06', 'John', 'Z', 'Sales Rep.', '10'],
['', '2B02', 'Jane', 'DOE', 'Finance Manager', '10'],
['', '', '', '', '', '20'],
Expand Down Expand Up @@ -941,12 +941,12 @@ describe('ExcelExportService', () => {
{ metadata: { style: 1, }, value: 'Position', },
{ metadata: { style: 1, }, value: 'Order', },
],
[' Order: 20 (2 items)'],
[' Last Name: Z (1 items)'], // expanded
[' Order: 20 (2 items)'],
[' Last Name: Z (1 items)'], // expanded
['', '1E06', 'John', 'Z', 'Sales Rep.', '10'],
[' Last Name: Doe (1 items)'], // expanded
[' Last Name: Doe (1 items)'], // expanded
['', '2B02', 'Jane', 'DOE', 'Finance Manager', '10'],
[' Last Name: null (0 items)'], // collapsed
[' Last Name: null (0 items)'], // collapsed
['', '', '', '', '', '20'],
['', '', '', '', '', '10'],
]
Expand Down Expand Up @@ -1365,8 +1365,8 @@ describe('ExcelExportService', () => {

describe('grid with colspan', () => {
let mockCollection;
let oddMetatadata = { columns: { lastName: { colspan: 2 } } } as ItemMetadata;
let evenMetatadata = { columns: { 0: { colspan: '*' } } } as ItemMetadata;
const oddMetatadata = { columns: { lastName: { colspan: 2 } } } as ItemMetadata;
const evenMetatadata = { columns: { 0: { colspan: '*' } } } as ItemMetadata;

beforeEach(() => {
mockGridOptions.enableTranslate = true;
Expand Down
4 changes: 2 additions & 2 deletions packages/excel-export/src/excelExport.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,8 +595,8 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ
const groupName = sanitizeHtmlToText(itemObj.title);

if (this._excelExportOptions && this._excelExportOptions.addGroupIndentation) {
const collapsedSymbol = this._excelExportOptions && this._excelExportOptions.groupCollapsedSymbol || '\u25B9';
const expandedSymbol = this._excelExportOptions && this._excelExportOptions.groupExpandedSymbol || '\u25BF';
const collapsedSymbol = this._excelExportOptions && this._excelExportOptions.groupCollapsedSymbol || '';
const expandedSymbol = this._excelExportOptions && this._excelExportOptions.groupExpandedSymbol || '';
const chevron = itemObj.collapsed ? collapsedSymbol : expandedSymbol;
return chevron + ' ' + addWhiteSpaces(5 * itemObj.level) + groupName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ export interface ExcelExportOption {
/** The default text to display in 1st column of the File Export, which will identify that the current row is a Grouping Aggregator */
groupingAggregatorRowText?: string;

/** Symbol use to show that the group title is collapsed (you can use unicode like '\u25B9' or '\u25B7') */
/** 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 '\u25BF' or '\u25BD') */
/** 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. */
Expand Down

0 comments on commit 8c4c2b8

Please sign in to comment.