Skip to content

Commit

Permalink
fix(tree): Tree Data export should also include correct indentation
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed May 12, 2021
1 parent c22165b commit f1e06c1
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class Example5 {
if (dataContext.treeLevel > 0) {
prefix = `<span class="mdi mdi-subdirectory-arrow-right mdi-v-align-sub color-se-secondary"></span>`;
}
return `${prefix}<span class="bold">${value}</span><span style="font-size:11px; margin-left: 15px;">(parentId: ${dataContext.parentId})</span>`;
return `${prefix}<span class="bold">${value}</span> <span style="font-size:11px; margin-left: 15px;">(parentId: ${dataContext.parentId})</span>`;
},
},
multiColumnSort: false, // multi-column sorting is not supported with Tree Data, so you need to disable it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ describe('Tree Export Formatter', () => {
expect(output).toBe('');
});

it('should return a span without any icon and ', () => {
it('should return a span without any icon which include leading char and 4 spaces to cover width of collapsing icons', () => {
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`);
expect(output).toBe(`. John`); // 3x spaces for exportIndentationLeadingSpaceCount + 1x space for space after collapsing icon in final string output
});

it('should return a span without any icon and 15px indentation of a tree level 1', () => {
Expand All @@ -70,7 +70,7 @@ describe('Tree Export Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

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

it('should return a span without any icon and 30px indentation of a tree level 2', () => {
Expand All @@ -79,7 +79,7 @@ describe('Tree Export Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

const output = treeExportFormatter(1, 1, dataset[2]['firstName'], {} as Column, dataset[2], gridStub);
expect(output).toBe(`. Bob`);
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', () => {
Expand All @@ -88,7 +88,7 @@ describe('Tree Export Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[2]);

const output = treeExportFormatter(1, 1, dataset[1]['firstName'], {} as Column, dataset[1], gridStub);
expect(output).toBe(` Jane`);
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', () => {
Expand All @@ -97,7 +97,7 @@ describe('Tree Export Formatter', () => {
jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValue(dataset[1]);

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

it('should execute "queryFieldNameGetterFn" callback to get field name to use when it is defined', () => {
Expand All @@ -107,7 +107,7 @@ describe('Tree Export Formatter', () => {

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`);
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', () => {
Expand All @@ -117,7 +117,7 @@ describe('Tree Export Formatter', () => {

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`);
expect(output).toBe(`⮞ Anonymous < Doe`);
});

it('should execute "queryFieldNameGetterFn" callback to get field name, which has (.) dot notation reprensenting complex object', () => {
Expand All @@ -127,6 +127,33 @@ describe('Tree Export Formatter', () => {

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`);
expect(output).toBe(`⮞ 444444`);
});

it('should return a span with expanded icon and 15px indentation of a tree level 1 with a value prefix when provided', () => {
mockGridOptions.treeDataOptions.levelPropName = 'indent';
mockGridOptions.treeDataOptions.titleFormatter = (_row, _cell, value, _def, dataContext) => `++${value}++`;
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[3]['firstName'], { field: 'firstName' } as Column, dataset[3], gridStub);
expect(output).toEqual(`⮞ ++Barbara++`);
});

it('should return a span with expanded icon and expected indentation and expanded icon of a tree level 1 with a value prefix when provided', () => {
mockGridOptions.treeDataOptions.levelPropName = 'indent';
mockGridOptions.treeDataOptions.titleFormatter = (_row, _cell, value, _def, dataContext) => {
if (dataContext.indent > 0) {
return `++${value}++`;
}
return value || '';
};
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'], indent: 1 }, { field: 'firstName' } as Column, dataset[1], gridStub);
expect(output).toEqual(`. ⮟ ++Jane++`);
});
});
42 changes: 28 additions & 14 deletions packages/common/src/formatters/treeExportFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { SlickDataView, Formatter } from './../interfaces/index';
import { addWhiteSpaces, getDescendantProperty } from '../services/utilities';
import { addWhiteSpaces, getDescendantProperty, sanitizeHtmlToText, } from '../services/utilities';
import { parseFormatterWhenExist } from './formatterUtilities';

/** 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();
export const treeExportFormatter: Formatter = (row, cell, value, columnDef, dataContext, grid) => {
const dataView = grid.getData<SlickDataView>();
const gridOptions = grid.getOptions();
const treeDataOptions = gridOptions?.treeDataOptions;
const collapsedPropName = treeDataOptions?.collapsedPropName ?? '__collapsed';
const treeLevelPropName = treeDataOptions?.levelPropName ?? '__treeLevel';
const indentMarginLeft = treeDataOptions?.exportIndentMarginLeft ?? 4;
const indentMarginLeft = treeDataOptions?.exportIndentMarginLeft ?? 5;
const exportIndentationLeadingChar = treeDataOptions?.exportIndentationLeadingChar ?? '.';
const exportIndentationLeadingSpaceCount = treeDataOptions?.exportIndentationLeadingSpaceCount ?? 3;
const groupCollapsedSymbol = gridOptions?.excelExportOptions?.groupCollapsedSymbol ?? '⮞';
const groupExpandedSymbol = gridOptions?.excelExportOptions?.groupExpandedSymbol ?? '⮟';
let outputValue = value;
Expand All @@ -29,20 +33,30 @@ export const treeExportFormatter: Formatter = (_row, _cell, value, columnDef, da
}

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

if (nextItemRow?.[treeLevelPropName] > treeLevel) {
if (dataContext.__collapsed) {
return `${groupCollapsedSymbol} ${spacer} ${outputValue}`;
} else {
return `${groupExpandedSymbol} ${spacer} ${outputValue}`;
}
toggleSymbol = dataContext?.[collapsedPropName] ? groupCollapsedSymbol : groupExpandedSymbol; // parent with child will have a toggle icon
indentation = treeLevel === 0 ? 0 : (indentMarginLeft * treeLevel);
} else {
indentation = (indentMarginLeft * (treeLevel === 0 ? 0 : treeLevel + 1));
}
const indentSpacer = addWhiteSpaces(indentation);

if (treeDataOptions?.titleFormatter) {
outputValue = parseFormatterWhenExist(treeDataOptions.titleFormatter, row, cell, columnDef, dataContext, grid);
}
return treeLevel === 0 ? outputValue : `.${spacer} ${outputValue}`;

const leadingChar = (treeLevel === 0 && toggleSymbol) ? '' : (treeLevel === 0 ? `${exportIndentationLeadingChar}${addWhiteSpaces(exportIndentationLeadingSpaceCount)}` : exportIndentationLeadingChar);
outputValue = `${leadingChar}${indentSpacer}${toggleSymbol} ${outputValue}`;
const sanitizedOutputValue = sanitizeHtmlToText(outputValue); // also remove any html tags that might exist

return sanitizedOutputValue;
}
return '';
};
2 changes: 1 addition & 1 deletion packages/common/src/formatters/treeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const treeFormatter: Formatter = (row, cell, value, columnDef, dataContex

if (dataView?.getItemByIdx) {
const identifierPropName = dataView.getIdPropertyName() ?? 'id';
const treeLevel = dataContext[treeLevelPropName] || 0;
const treeLevel = dataContext?.[treeLevelPropName] ?? 0;
const indentSpacer = `<span style="display:inline-block; width:${indentMarginLeft * treeLevel}px;"></span>`;
const idx = dataView.getIdxById(dataContext[identifierPropName]);
const nextItemRow = dataView.getItemByIdx((idx || 0) + 1);
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/global-grid-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ export const GlobalGridOptions: GridOption = {
resizeDefaultRatioForStringType: 0.88,
resizeMaxItemToInspectCellContentWidth: 1000,
treeDataOptions: {
exportIndentMarginLeft: 4,
exportIndentationLeadingChar: '.',
exportIndentMarginLeft: 5,
exportIndentationLeadingChar: '͏͏͏͏͏͏͏͏͏·',
} as unknown as TreeDataOption
};

Expand Down
10 changes: 8 additions & 2 deletions packages/common/src/interfaces/treeDataOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,23 @@ export interface TreeDataOption {
indentMarginLeft?: number;

/**
* Defaults to 4, indentation spaces to add from the left (calculated by the tree level multiplied by this number).
* Defaults to 5, 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
* Defaults to centered 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;

/**
* Defaults to 3, when using a collapsing icon then we need to add some extra spaces to compensate on parent level.
* If you don't want collapsing icon in your export then you probably want to put this option at 0.
*/
exportIndentationLeadingSpaceCount?: number;

/** Optional Title Formatter (allows you to format/style the title text differently) */
titleFormatter?: Formatter;
}
Binary file not shown.

0 comments on commit f1e06c1

Please sign in to comment.