Skip to content

Commit

Permalink
Merge pull request #753 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 #754
  • Loading branch information
ghiscoding authored May 5, 2021
2 parents 71163bd + 4450bc4 commit 6eba7bb
Show file tree
Hide file tree
Showing 30 changed files with 855 additions and 243 deletions.
1 change: 1 addition & 0 deletions src/app/examples/grid-grouping.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export class GridGroupingComponent implements OnInit {
// you could debounce/throttle the input text filter if you have lots of data
// filterTypingDebounce: 250,
enableGrouping: true,
enableExport: true,
exportOptions: {
sanitizeDataExport: true
},
Expand Down
2 changes: 1 addition & 1 deletion src/app/examples/grid-resize-by-content.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const myCustomTitleValidator = (value: any, args: any) => {
@Injectable()
export class GridResizeByContentComponent implements OnInit {
title = 'Example 30: Columns Resize by Content';
subTitle = `The grid below uses the optional resize by cell content (with a fixed 950px for demo purposes), you can click on the 2 buttons to see the difference. The "autosizeColumns" is really the default option used by SlickGrid-Universal, the resize by cell content is optional because it requires to read the first thousand rows and do extra width calculation.`;
subTitle = `The grid below uses the optional resize by cell content (with a fixed 950px for demo purposes), you can click on the 2 buttons to see the difference. The "autosizeColumns" is really the default option used by Angular-SlickGrid, the resize by cell content is optional because it requires to read the first thousand rows and do extra width calculation.`;

angularGrid!: AngularGridInstance;
gridOptions!: GridOption;
Expand Down
8 changes: 4 additions & 4 deletions src/app/examples/grid-tree-data-hierarchical.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ <h2>
<span>Expand All</span>
</button>
<button (click)="logFlatStructure()" class="btn btn-outline-secondary btn-sm">
<span>Log Flag Structure</span>
<span>Log Flat Structure</span>
</button>
<button (click)="logExpandedStructure()" class="btn btn-outline-secondary btn-sm">
<span>Log Expanded Structure</span>
<button (click)="logHierarchicalStructure()" class="btn btn-outline-secondary btn-sm">
<span>Log Hierarchical Structure</span>
</button>
</div>

Expand All @@ -54,4 +54,4 @@ <h2>
[datasetHierarchical]="datasetHierarchical"
(onAngularGridCreated)="angularGridReady($event)">
</angular-slickgrid>
</div>
</div>
7 changes: 4 additions & 3 deletions src/app/examples/grid-tree-data-hierarchical.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export class GridTreeDataHierarchicalComponent implements OnInit {
// change header/cell row height for salesforce theme
headerRowHeight: 35,
rowHeight: 33,
showCustomFooter: true,

// use Material Design SVG icons
contextMenu: {
Expand Down Expand Up @@ -218,12 +219,12 @@ export class GridTreeDataHierarchicalComponent implements OnInit {
this.angularGrid.treeDataService.toggleTreeDataCollapse(false);
}

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

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

mockDataset() {
Expand Down
11 changes: 7 additions & 4 deletions src/app/examples/grid-tree-data-parent-child.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ <h2>
<span>Expand All</span>
</button>
<button (click)="logFlatStructure()" class="btn btn-outline-secondary btn-sm">
<span>Log Flag Structure</span>
<span>Log Flat Structure</span>
</button>
<button (click)="logExpandedStructure()" class="btn btn-outline-secondary btn-sm">
<span>Log Expanded Structure</span>
<button (click)="logHierarchicalStructure()" class="btn btn-outline-secondary btn-sm">
<span>Log Hierarchical Structure</span>
</button>
<button (click)="dynamicallyChangeFilter()" class="btn btn-outline-secondary btn-sm">
<span>Dynamically Change Filter (% complete &lt; 40)</span>
</button>
</div>
</div>
Expand All @@ -42,4 +45,4 @@ <h2>
[dataset]="dataset"
(onAngularGridCreated)="angularGridReady($event)">
</angular-slickgrid>
</div>
</div>
83 changes: 45 additions & 38 deletions src/app/examples/grid-tree-data-parent-child.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ export class GridTreeDataParentChildComponent implements OnInit {
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.percentCompleteBar, type: FieldType.number,
},
Expand All @@ -77,6 +79,7 @@ export class GridTreeDataParentChildComponent implements OnInit {
},
{
id: 'effortDriven', name: 'Effort Driven', width: 80, minWidth: 20, maxWidth: 80, cssClass: 'cell-effort-driven', field: 'effortDriven',
exportWithFormatter: false,
formatter: Formatters.checkmark, cannotTriggerInsert: true,
filterable: true,
filter: {
Expand All @@ -95,12 +98,21 @@ export class GridTreeDataParentChildComponent implements OnInit {
enableAutoResize: true,
enableExport: true,
enableFiltering: true,
exportOptions: { exportWithFormatter: true },
excelExportOptions: { exportWithFormatter: true },
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'
}
},
showCustomFooter: true,
multiColumnSort: false, // multi-column sorting is not supported with Tree Data, so you need to disable it
// change header/cell row height for material design theme
headerRowHeight: 45,
Expand Down Expand Up @@ -147,44 +159,35 @@ export class GridTreeDataParentChildComponent implements OnInit {
}

/**
* 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.dataViewObj.getItemCount();
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.dataViewObj.getItems().find((item: any) => item[treeLevelPropName] === newTreeLevel);
const parentItemFound = this.dataViewObj.getItemByIdx(childItemFound[parentPropName]);

const newItem = {
id: newId,
indent: newTreeLevel,
parentId: parentItemFound.id,
title: `Task ${newId}`,
duration: '1 day',
percentComplete: Math.round(Math.random() * 100),
start: new Date(),
finish: new Date(),
effortDriven: false
};
this.dataViewObj.addItem(newItem);
const dataset = this.dataViewObj.getItems();
this.dataset = [...dataset]; // make a copy to trigger a dataset refresh

// add setTimeout to wait a full cycle because datasetChanged needs a full cycle
// force a resort because of the tree data structure
setTimeout(() => {
const titleColumn = this.columnDefinitions.find(col => col.id === 'title') as Column;
this.angularGrid.sortService.onLocalSortChanged(this.gridObj, [{ columnId: 'title', sortCol: titleColumn, sortAsc: true }]);

// scroll into the position, after insertion cycle, where the item was added
const rowIndex = this.dataViewObj.getRowById(newItem.id);
this.gridObj.scrollRowIntoView(rowIndex + 3);
}, 0);
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.angularGrid.gridService.addItem(newItem);
}
}

collapseAll() {
Expand All @@ -195,12 +198,17 @@ export class GridTreeDataParentChildComponent implements OnInit {
this.angularGrid.treeDataService.toggleTreeDataCollapse(false);
}

logExpandedStructure() {
console.log('exploded array', this.angularGrid.treeDataService.datasetHierarchical /* , JSON.stringify(explodedArray, null, 2) */);
dynamicallyChangeFilter() {
// const randomPercentage = Math.floor((Math.random() * 99));
this.angularGrid.filterService.updateFilters([{ columnId: 'percentComplete', operator: '<', searchTerms: [40] }]);
}

logHierarchicalStructure() {
console.log('exploded array', this.angularGrid.treeDataService.datasetHierarchical);
}

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

mockData(count: number) {
Expand Down Expand Up @@ -232,7 +240,6 @@ export class GridTreeDataParentChildComponent implements OnInit {
}

d['id'] = i;
d['indent'] = indent;
d['parentId'] = parentId;
d['title'] = 'Task ' + i;
d['duration'] = '5 days';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const filterServiceStub = {
bindLocalOnSort: jest.fn(),
bindBackendOnSort: jest.fn(),
populateColumnFilterSearchTermPresets: jest.fn(),
refreshTreeDataFilters: jest.fn(),
getColumnFilters: jest.fn(),
} as unknown as FilterService;

Expand Down Expand Up @@ -150,12 +151,16 @@ const sortServiceStub = {
dispose: jest.fn(),
loadGridSorters: jest.fn(),
processTreeDataInitialSort: jest.fn(),
sortHierarchicalDataset: jest.fn(),
} as unknown as SortService;

const treeDataServiceStub = {
convertFlatDatasetConvertToHierarhicalView: jest.fn(),
init: jest.fn(),
convertToHierarchicalDatasetAndSort: jest.fn(),
dispose: jest.fn(),
handleOnCellClick: jest.fn(),
sortHierarchicalDataset: jest.fn(),
toggleTreeDataCollapse: jest.fn(),
} as unknown as TreeDataService;

Expand Down Expand Up @@ -1611,59 +1616,81 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () =
});

describe('Tree Data View', () => {
it('should throw an error when enableTreeData is enabled with Pagination since that is not supported', (done) => {
try {
component.gridOptions = { enableTreeData: true, enablePagination: true } as GridOption;
component.ngAfterViewInit();
} catch (e) {
expect(e.toString()).toContain('[Angular-Slickgrid] It looks like you are trying to use Tree Data with Pagination but unfortunately that is simply not supported because of its complexity.');
component.destroy();
done();
}
});

it('should throw an error when enableTreeData is enabled without passing a "columnId"', (done) => {
try {
component.gridOptions = { enableTreeData: true, treeDataOptions: {} } as GridOption;
component.ngAfterViewInit();
} catch (e) {
expect(e.toString()).toContain('[Angular-Slickgrid] When enabling tree data, you must also provide the "treeDataOption" property in your Grid Options with "childrenPropName" or "parentPropName"');
component.destroy();
done();
}
afterEach(() => {
component.destroy();
jest.clearAllMocks();
});

it('should change flat dataset and expect being called with other methods', () => {
it('should change flat dataset and expect being called with other methods', (done) => {
const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }];
const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }] }];
const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set');
jest.spyOn(treeDataServiceStub, 'convertToHierarchicalDatasetAndSort').mockReturnValue({ hierarchical: mockHierarchical, flat: mockFlatDataset });
const refreshTreeSpy = jest.spyOn(filterServiceStub, 'refreshTreeDataFilters');

component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file', parentPropName: 'parentId', childrenPropName: 'files' } } as GridOption;
component.ngAfterViewInit();
component.dataset = mockFlatDataset;

setTimeout(() => {
expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical);
expect(refreshTreeSpy).not.toHaveBeenCalled();
done();
})
});

it('should change hierarchical dataset and expect processTreeDataInitialSort being called with other methods', () => {
const mockHierarchical = [{ file: 'documents', files: [{ file: 'vacation.txt' }] }];
const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set');
const clearFilterSpy = jest.spyOn(filterServiceStub, 'clearFilters');
const setItemsSpy = jest.spyOn(mockDataView, 'setItems');
const processSpy = jest.spyOn(sortServiceStub, 'processTreeDataInitialSort');

component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file' } } as unknown as GridOption;
component.ngAfterViewInit();
component.datasetHierarchical = mockHierarchical;

expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical);
expect(clearFilterSpy).toHaveBeenCalled();
expect(processSpy).toHaveBeenCalled();
expect(setItemsSpy).toHaveBeenCalledWith([], 'id');
});

it('should change hierarchical dataset and expect processTreeDataInitialSort being called with other methods', (done) => {
it('should preset hierarchical dataset before the initialization and expect sortHierarchicalDataset to be called', () => {
const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }];
const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }] }];
const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set');
const clearFilterSpy = jest.spyOn(filterServiceStub, 'clearFilters');
const setItemsSpy = jest.spyOn(mockDataView, 'setItems');
const processSpy = jest.spyOn(sortServiceStub, 'processTreeDataInitialSort');
const sortHierarchicalSpy = jest.spyOn(treeDataServiceStub, 'sortHierarchicalDataset').mockReturnValue({ hierarchical: mockHierarchical, flat: mockFlatDataset });

component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file' } } as GridOption;
component.destroy();
component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file' } } as unknown as GridOption;
component.datasetHierarchical = mockHierarchical;
component.ngAfterViewInit();

setTimeout(() => {
expect(component.datasetHierarchical).toEqual(mockHierarchical);
expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical);
expect(clearFilterSpy).toHaveBeenCalled();
expect(processSpy).toHaveBeenCalled();
expect(setItemsSpy).toHaveBeenCalledWith([], 'id');
done();
}, 2);
expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical);
expect(clearFilterSpy).toHaveBeenCalled();
expect(processSpy).not.toHaveBeenCalled();
expect(setItemsSpy).toHaveBeenCalledWith(mockFlatDataset, 'id');
expect(sortHierarchicalSpy).toHaveBeenCalledWith(mockHierarchical);
});

it('should expect "refreshTreeDataFilters" method to be called when our flat dataset was already set and it just got changed a 2nd time', () => {
const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }];
const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }] }];
const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set');
jest.spyOn(treeDataServiceStub, 'convertToHierarchicalDatasetAndSort').mockReturnValue({ hierarchical: mockHierarchical, flat: mockFlatDataset });
const refreshTreeSpy = jest.spyOn(filterServiceStub, 'refreshTreeDataFilters');

component.dataset = [{ id: 0, file: 'documents' }];
component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file', parentPropName: 'parentId', childrenPropName: 'files' } } as GridOption;
component.ngAfterViewInit();
component.dataset = mockFlatDataset;

expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical);
expect(refreshTreeSpy).toHaveBeenCalled();
});
});
});
Expand Down
Loading

0 comments on commit 6eba7bb

Please sign in to comment.