Skip to content

Commit

Permalink
Merge pull request #326 from ghiscoding/bugfix/tree-data-initial-sort
Browse files Browse the repository at this point in the history
fix(tree): few issues found with Tree Data, fixes #307
  • Loading branch information
ghiscoding authored May 5, 2021
2 parents c72a4e7 + 8c4c2b8 commit 3d6e51c
Show file tree
Hide file tree
Showing 28 changed files with 808 additions and 223 deletions.
12 changes: 8 additions & 4 deletions examples/webpack-demo-vanilla-bundle/src/examples/example05.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@ <h6 class="title is-6 italic">
<span>Expand All</span>
</button>
<button onclick.delegate="logFlatStructure()" class="button is-small">
<span>Log Flag Structure</span>
<span>Log Flat Structure</span>
</button>
<button onclick.delegate="logExpandedStructure()" class="button is-small">
<span>Log Expanded Structure</span>
<button onclick.delegate="logHierarchicalStructure()" class="button is-small">
<span>Log Hierarchical Structure</span>
</button>
<button onclick.delegate="dynamicallyChangeFilter()" class="button is-small">
<span class="icon mdi mdi-filter-outline"></span>
<span>Dynamically Change Filter (% complete &lt; 40)</span>
</button>
</div>
</div>

<div class="grid5">
</div>
</div>
93 changes: 47 additions & 46 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 All @@ -92,8 +93,14 @@ export class Example5 {
enableTreeData: true, // you must enable this flag for the filtering & sorting to work as expected
treeDataOptions: {
columnId: 'title',
levelPropName: 'indent',
parentPropName: 'parentId'
// levelPropName: 'indent', // this is optional, you can define the tree level property name that will be used for the sorting/indentation, internally it will use "__treeLevel"
parentPropName: 'parentId',

// you can optionally sort by a different column and/or sort direction
initialSort: {
columnId: 'title',
direction: 'ASC'
}
},
multiColumnSort: false, // multi-column sorting is not supported with Tree Data, so you need to disable it
presets: {
Expand All @@ -103,44 +110,34 @@ export class Example5 {
}

/**
* A simple method to add a new item inside the first group that we find.
* A simple method to add a new item inside the first group that we find (it's random and is only for demo purposes).
* After adding the item, it will sort by parent/child recursively
*/
addNewRow() {
const newId = this.dataset.length;
const newId = this.sgb.dataset.length;
const parentPropName = 'parentId';
const treeLevelPropName = 'indent';
const treeLevelPropName = '__treeLevel'; // if undefined in your options, the default prop name is "__treeLevel"
const newTreeLevel = 1;

// find first parent object and add the new item as a child
const childItemFound = this.dataset.find((item) => item[treeLevelPropName] === newTreeLevel);
const childItemFound = this.sgb.dataset.find((item) => item[treeLevelPropName] === newTreeLevel);
const parentItemFound = this.sgb.dataView.getItemByIdx(childItemFound[parentPropName]);

const newItem = {
id: newId,
indent: newTreeLevel,
parentId: parentItemFound.id,
title: `Task ${newId}`,
duration: '1 day',
percentComplete: 0,
start: new Date(),
finish: new Date(),
effortDriven: false
};
this.sgb.dataView.addItem(newItem);
this.dataset = this.sgb.dataView.getItems();
this.sgb.dataset = this.dataset;

// force a resort
const titleColumn = this.columnDefinitions.find((col) => col.id === 'title');
this.sgb.sortService.onLocalSortChanged(this.sgb.slickGrid, [{ columnId: 'title', sortCol: titleColumn, sortAsc: true }]);

// update dataset and re-render (invalidate) the grid
this.sgb.slickGrid.invalidate();

// scroll to the new row
const rowIndex = this.sgb.dataView.getIdxById(newItem.id);
this.sgb.slickGrid.scrollRowIntoView(rowIndex, false);
if (childItemFound && parentItemFound) {
const newItem = {
id: newId,
parentId: parentItemFound.id,
title: `Task ${newId}`,
duration: '1 day',
percentComplete: 99,
start: new Date(),
finish: new Date(),
effortDriven: false
};

// use the Grid Service to insert the item,
// it will also internally take care of updating & resorting the hierarchical dataset
this.sgb.gridService.addItem(newItem);
}
}

collapseAll() {
Expand All @@ -151,12 +148,16 @@ export class Example5 {
this.sgb.treeDataService.toggleTreeDataCollapse(false);
}

logExpandedStructure() {
console.log('exploded array', this.sgb.treeDataService.datasetHierarchical /* , JSON.stringify(explodedArray, null, 2) */);
dynamicallyChangeFilter() {
this.sgb.filterService.updateFilters([{ columnId: 'percentComplete', operator: '<', searchTerms: [40] }]);
}

logHierarchicalStructure() {
console.log('hierarchical array', this.sgb.treeDataService.datasetHierarchical);
}

logFlatStructure() {
console.log('flat array', this.sgb.treeDataService.dataset /* , JSON.stringify(outputFlatArray, null, 2) */);
console.log('flat array', this.sgb.treeDataService.dataset);
}

mockDataset() {
Expand Down Expand Up @@ -188,9 +189,9 @@ export class Example5 {
}

d['id'] = i;
d['indent'] = indent;
d['parentId'] = parentId;
d['title'] = 'Task ' + i;
// d['title'] = `Task ${i} - [P]: ${parentId}`;
d['title'] = `Task ${i}`;
d['duration'] = '5 days';
d['percentComplete'] = Math.round(Math.random() * 100);
d['start'] = new Date(randomYear, randomMonth, randomDay);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ <h6 class="title is-6 italic">
<span>Expand All</span>
</button>
<button onclick.delegate="logFlatStructure()" class="button is-small">
<span>Log Flag Structure</span>
<span>Log Flat Structure</span>
</button>
<button onclick.delegate="logExpandedStructure()" class="button is-small">
<span>Log Expanded Structure</span>
<button onclick.delegate="logHierarchicalStructure()" class="button is-small">
<span>Log Hierarchical Structure</span>
</button>
</div>
<div class="column is-6">
Expand Down Expand Up @@ -58,4 +58,4 @@ <h6 class="title is-6 italic">
</div>

<div class="grid6">
</div>
</div>
19 changes: 11 additions & 8 deletions examples/webpack-demo-vanilla-bundle/src/examples/example06.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ export class Example6 {
// columnId: 'file',
// direction: 'DESC'
// }
}
},
showCustomFooter: true,
};
}

Expand Down Expand Up @@ -142,7 +143,7 @@ export class Example6 {
* After adding the item, it will sort by parent/child recursively
*/
addNewFile() {
const newId = this.sgb.dataView.getLength() + 100;
const newId = this.sgb.dataView.getItemCount() + 100;

// find first parent object and add the new item as a child
const popItem = findItemInHierarchicalStructure(this.datasetHierarchical, x => x.file === 'pop', 'files');
Expand All @@ -158,9 +159,11 @@ export class Example6 {
// overwrite hierarchical dataset which will also trigger a grid sort and rendering
this.sgb.datasetHierarchical = this.datasetHierarchical;

// scroll into the position where the item was added
const rowIndex = this.sgb.dataView.getRowById(popItem.id);
this.sgb.slickGrid.scrollRowIntoView(rowIndex + 3);
// scroll into the position where the item was added with a delay since it needs to recreate the tree grid
setTimeout(() => {
const rowIndex = this.sgb.dataView.getRowById(popItem.id);
this.sgb.slickGrid.scrollRowIntoView(rowIndex + 3);
}, 0);
}
}

Expand All @@ -172,12 +175,12 @@ export class Example6 {
this.sgb.treeDataService.toggleTreeDataCollapse(false);
}

logExpandedStructure() {
console.log('exploded array', this.sgb.treeDataService.datasetHierarchical /* , JSON.stringify(explodedArray, null, 2) */);
logHierarchicalStructure() {
console.log('hierarchical array', this.sgb.treeDataService.datasetHierarchical);
}

logFlatStructure() {
console.log('flat array', this.sgb.treeDataService.dataset /* , JSON.stringify(outputFlatArray, null, 2) */);
console.log('flat array', this.sgb.treeDataService.dataset);
}

mockDataset() {
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`);
});
});
Loading

0 comments on commit 3d6e51c

Please sign in to comment.