Skip to content

Commit

Permalink
fix(exports): grid with colspan should be export accordingly (#311)
Browse files Browse the repository at this point in the history
- read each item metadata to evaluate if the row has any colspan defined and if so then export them accordingly (in Excel we would merge the cells but in regular text export then we would just return empty string)
  • Loading branch information
ghiscoding authored Apr 16, 2021
1 parent 09a4cd3 commit e899fbb
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 98 deletions.
21 changes: 11 additions & 10 deletions examples/webpack-demo-vanilla-bundle/src/examples/example08.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OperatorString,
} from '@slickgrid-universal/common';
import { ExcelExportService } from '@slickgrid-universal/excel-export';
import { TextExportService } from '@slickgrid-universal/text-export';
import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';

import { ExampleGridOptions } from './example-grid-options';
Expand Down Expand Up @@ -51,7 +52,7 @@ export class Example08 {
definedGrid1() {
this.columnDefinitions1 = [
{ id: 'title', name: 'Title', field: 'title', sortable: true, columnGroup: 'Common Factor' },
{ id: 'duration', name: 'Duration', type: FieldType.number, field: 'duration', columnGroup: 'Common Factor' },
{ id: 'duration', name: 'Duration', field: 'duration', columnGroup: 'Common Factor' },
{ id: 'start', name: 'Start', field: 'start', columnGroup: 'Period' },
{ id: 'finish', name: 'Finish', field: 'finish', columnGroup: 'Period' },
{ id: '%', name: '% Complete', type: FieldType.number, field: 'percentComplete', selectable: false, columnGroup: 'Analysis' },
Expand All @@ -62,12 +63,13 @@ export class Example08 {
enableAutoResize: false,
gridHeight: 275,
gridWidth: 800,
enableTextExport: true,
enableExcelExport: true,
excelExportOptions: {
exportWithFormatter: true,
sanitizeDataExport: true
},
registerExternalResources: [new ExcelExportService()],
registerExternalResources: [new TextExportService(), new ExcelExportService()],
enableCellNavigation: true,
enableColumnReorder: false,
enableSorting: true,
Expand Down Expand Up @@ -150,15 +152,14 @@ export class Example08 {
}
}
};
} else {
return {
columns: {
0: {
colspan: '*' // starting at column index 0, we will span accross all column (*)
}
}
};
}
return {
columns: {
0: {
colspan: '*' // starting at column index 0, we will span accross all column (*)
}
}
};
}

//
Expand Down
33 changes: 22 additions & 11 deletions packages/common/src/services/__tests__/export-utilities.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { exportWithFormatterWhenDefined } from '../export-utilities';
import { Column, Formatter, SlickGrid } from '../../interfaces/index';

const mockDataView = {
constructor: jest.fn(),
init: jest.fn(),
destroy: jest.fn(),
getItemMetadata: jest.fn(),
};

const gridStub = {
getData: () => mockDataView,
};

describe('Export Utilities', () => {
let mockItem;
let mockColumn: Column;
Expand All @@ -14,60 +25,60 @@ describe('Export Utilities', () => {

describe('exportWithFormatterWhenDefined method', () => {
it('should NOT enable exportWithFormatter and expect the firstName to returned', () => {
const output = exportWithFormatterWhenDefined(1, 1, mockItem, mockColumn, {} as SlickGrid, { exportWithFormatter: false });
const output = exportWithFormatterWhenDefined(1, 1, mockItem, mockColumn, gridStub as SlickGrid, { exportWithFormatter: false });
expect(output).toBe('John');
});

it('should provide a column definition field defined with a dot (.) notation and expect a complex object result', () => {
const output = exportWithFormatterWhenDefined(1, 1, mockItem, { ...mockColumn, field: 'address.zip' }, {} as SlickGrid, {});
const output = exportWithFormatterWhenDefined(1, 1, mockItem, { ...mockColumn, field: 'address.zip' }, gridStub as SlickGrid, {});
expect(output).toEqual({ zip: 12345 });
});

it('should provide a column definition field defined with a dot (.) notation and expect an empty string when the complex result is an empty object', () => {
const output = exportWithFormatterWhenDefined(1, 1, mockItem, { ...mockColumn, field: 'empty' }, {} as SlickGrid, {});
const output = exportWithFormatterWhenDefined(1, 1, mockItem, { ...mockColumn, field: 'empty' }, gridStub as SlickGrid, {});
expect(output).toEqual('');
});

it('should provide a exportCustomFormatter in the column definition and expect the output to be formatted', () => {
const output = exportWithFormatterWhenDefined(1, 1, mockItem, { ...mockColumn, exportCustomFormatter: myBoldHtmlFormatter }, {} as SlickGrid, { exportWithFormatter: true });
const output = exportWithFormatterWhenDefined(1, 1, mockItem, { ...mockColumn, exportCustomFormatter: myBoldHtmlFormatter }, gridStub as SlickGrid, { exportWithFormatter: true });
expect(output).toBe('<b>John</b>');
});

it('should provide a exportCustomFormatter in the column definition and expect empty string when associated item property is null', () => {
const output = exportWithFormatterWhenDefined(1, 1, { ...mockItem, firstName: null }, { ...mockColumn, exportCustomFormatter: myBoldHtmlFormatter }, {} as SlickGrid, { exportWithFormatter: true });
const output = exportWithFormatterWhenDefined(1, 1, { ...mockItem, firstName: null }, { ...mockColumn, exportCustomFormatter: myBoldHtmlFormatter }, gridStub as SlickGrid, { exportWithFormatter: true });
expect(output).toBe('');
});

it('should provide a exportCustomFormatter in the column definition and expect empty string when associated item property is undefined', () => {
const output = exportWithFormatterWhenDefined(1, 1, { ...mockItem, firstName: undefined }, { ...mockColumn, exportCustomFormatter: myBoldHtmlFormatter }, {} as SlickGrid, { exportWithFormatter: true });
const output = exportWithFormatterWhenDefined(1, 1, { ...mockItem, firstName: undefined }, { ...mockColumn, exportCustomFormatter: myBoldHtmlFormatter }, gridStub as SlickGrid, { exportWithFormatter: true });
expect(output).toBe('');
});

it('should enable exportWithFormatter as an exportOption and expect the firstName to be formatted', () => {
const output = exportWithFormatterWhenDefined(1, 1, mockItem, mockColumn, {} as SlickGrid, { exportWithFormatter: true });
const output = exportWithFormatterWhenDefined(1, 1, mockItem, mockColumn, gridStub as SlickGrid, { exportWithFormatter: true });
expect(output).toBe('JOHN');
});

it('should enable exportWithFormatter as a grid option and expect the firstName to be formatted', () => {
mockColumn.exportWithFormatter = true;
const output = exportWithFormatterWhenDefined(1, 1, mockItem, mockColumn, {} as SlickGrid, { exportWithFormatter: true });
const output = exportWithFormatterWhenDefined(1, 1, mockItem, mockColumn, gridStub as SlickGrid, { exportWithFormatter: true });
expect(output).toBe('JOHN');
});

it('should enable exportWithFormatter as a grid option and expect empty string when associated item property is null', () => {
mockColumn.exportWithFormatter = true;
const output = exportWithFormatterWhenDefined(1, 1, { ...mockItem, firstName: null }, mockColumn, {} as SlickGrid, { exportWithFormatter: true });
const output = exportWithFormatterWhenDefined(1, 1, { ...mockItem, firstName: null }, mockColumn, gridStub as SlickGrid, { exportWithFormatter: true });
expect(output).toBe('');
});

it('should enable exportWithFormatter as a grid option and expect empty string when associated item property is undefined', () => {
mockColumn.exportWithFormatter = true;
const output = exportWithFormatterWhenDefined(1, 1, { ...mockItem, firstName: undefined }, mockColumn, {} as SlickGrid, { exportWithFormatter: true });
const output = exportWithFormatterWhenDefined(1, 1, { ...mockItem, firstName: undefined }, mockColumn, gridStub as SlickGrid, { exportWithFormatter: true });
expect(output).toBe('');
});

it('should expect empty string when associated item property is undefined and has no formatter defined', () => {
const output = exportWithFormatterWhenDefined(1, 1, { ...mockItem, firstName: undefined }, mockColumn, {} as SlickGrid, {});
const output = exportWithFormatterWhenDefined(1, 1, { ...mockItem, firstName: undefined }, mockColumn, gridStub as SlickGrid, {});
expect(output).toBe('');
});
});
Expand Down
23 changes: 10 additions & 13 deletions packages/common/src/services/export-utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ export function exportWithFormatterWhenDefined(row: number, col: number, dataCon
isEvaluatingFormatter = !!columnDef.exportWithFormatter;
}

// did the user provide a Custom Formatter for the export
const exportCustomFormatter: Formatter | undefined = (columnDef.exportCustomFormatter !== undefined) ? columnDef.exportCustomFormatter : undefined;

// does the field have the dot (.) notation and is a complex object? if so pull the first property name
const fieldId = columnDef.field || columnDef.id || '';
let fieldProperty = fieldId;
Expand All @@ -27,17 +24,17 @@ export function exportWithFormatterWhenDefined(row: number, col: number, dataCon

const cellValue = dataContext.hasOwnProperty(fieldProperty) ? dataContext[fieldProperty] : null;

if (dataContext && exportCustomFormatter !== undefined) {
const formattedData = exportCustomFormatter(row, col, cellValue, columnDef, dataContext, grid);
output = formattedData as string;
if (formattedData && typeof formattedData === 'object' && formattedData.hasOwnProperty('text')) {
output = formattedData.text;
}
if (output === null || output === undefined) {
output = '';
}
let formatter: Formatter | undefined;
if (dataContext && columnDef.exportCustomFormatter) {
// did the user provide a Custom Formatter for the export
formatter = columnDef.exportCustomFormatter;
} else if (isEvaluatingFormatter && columnDef.formatter) {
const formattedData = columnDef.formatter(row, col, cellValue, columnDef, dataContext, grid);
// or else do we have a column Formatter AND are we evaluating it?
formatter = columnDef.formatter;
}

if (typeof formatter === 'function') {
const formattedData = formatter(row, col, cellValue, columnDef, dataContext, grid);
output = formattedData as string;
if (formattedData && typeof formattedData === 'object' && formattedData.hasOwnProperty('text')) {
output = formattedData.text;
Expand Down
83 changes: 81 additions & 2 deletions packages/excel-export/src/excelExport.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
GridOption,
GroupTotalsFormatter,
GroupTotalFormatters,
ItemMetadata,
PubSubService,
SlickDataView,
SlickGrid,
Expand Down Expand Up @@ -53,6 +54,7 @@ const myCustomObjectFormatter: Formatter = (_row, _cell, value, _columnDef, data
const dataViewStub = {
getGrouping: jest.fn(),
getItem: jest.fn(),
getItemMetadata: jest.fn(),
getLength: jest.fn(),
setGrouping: jest.fn(),
} as unknown as SlickDataView;
Expand Down Expand Up @@ -978,9 +980,9 @@ describe('ExcelExportService', () => {
],
['Order: 20 (2 items)'],
['Last Name: Z (1 items)'],
['', '1E06', 'John', 'Z', 'Sales Rep.', { metadata: { style: 3 }, value: '10', }],
['', '1E06', 'John', 'Z', 'Sales Rep.', { metadata: { style: 3, type: 'number' }, value: 10, }],
['Last Name: Doe (1 items)'],
['', '2B02', 'Jane', 'DOE', 'Finance Manager', { metadata: { style: 3 }, value: '10', }],
['', '2B02', 'Jane', 'DOE', 'Finance Manager', { metadata: { style: 3, type: 'number' }, value: 10, }],
['Last Name: null (0 items)'],
['', '', '', '', '', '20'],
['', '', '', '', '', '10'],
Expand Down Expand Up @@ -1360,6 +1362,83 @@ describe('ExcelExportService', () => {
});
});
});

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

beforeEach(() => {
mockGridOptions.enableTranslate = true;
mockGridOptions.translater = translateService;
mockGridOptions.excelExportOptions = {};
mockGridOptions.createPreHeaderPanel = false;
mockGridOptions.showPreHeaderPanel = false;
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: 'order', field: 'order', width: 100, },
] as Column[];

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

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

it('should return associated Excel column name when calling "getExcelColumnNameByIndex" method with a column index', () => {
const excelColumnA = service.getExcelColumnNameByIndex(1);
const excelColumnZ = service.getExcelColumnNameByIndex(26);
const excelColumnAA = service.getExcelColumnNameByIndex(27);
const excelColumnCA = service.getExcelColumnNameByIndex(79);

expect(excelColumnA).toBe('A');
expect(excelColumnZ).toBe('Z');
expect(excelColumnAA).toBe('AA');
expect(excelColumnCA).toBe('CA');
});

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: 1, userId: '1E09', firstName: 'Jane', lastName: 'Doe', position: 'DEVELOPER', order: 15 },
{ id: 2, userId: '2ABC', firstName: 'Sponge', lastName: 'Bob', position: 'IT_ADMIN', order: 33 },
];
jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length);
jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]).mockReturnValueOnce(mockCollection[1]).mockReturnValueOnce(mockCollection[2]);
jest.spyOn(dataViewStub, 'getItemMetadata').mockReturnValue(oddMetatadata).mockReturnValueOnce(evenMetatadata).mockReturnValueOnce(oddMetatadata).mockReturnValueOnce(evenMetatadata);
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');
const spyUrlCreate = jest.spyOn(URL, 'createObjectURL');
const spyDownload = jest.spyOn(service, 'startDownloadFile');

const optionExpectation = { filename: 'export.xlsx', format: FileType.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: 'User Id', },
{ metadata: { style: 1, }, value: 'First Name', },
{ metadata: { style: 1, }, value: 'Last Name', },
{ metadata: { style: 1, }, value: 'Position', },
{ metadata: { style: 1, }, value: 'Order', },
],
['1E06', '', '', ''],
['1E09', 'Jane', 'DOE', '', 15],
['2ABC', '', '', ''],
]
});
});
});
});

describe('without Translater Service', () => {
Expand Down
Loading

0 comments on commit e899fbb

Please sign in to comment.