Skip to content

Commit 0b120f4

Browse files
committed
fix(tree): couple of issues found in Tree Data
1. initial sort not always working 2. tree level property should not be required while providing a `parentId` relation 3. tree data was loading and rendering the grid more than once (at least 1x time before the sort and another time after the tree was built) while it should only be rendered once
1 parent 49437d8 commit 0b120f4

15 files changed

+255
-118
lines changed

src/app/examples/grid-resize-by-content.component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const myCustomTitleValidator = (value: any, args: any) => {
5454
@Injectable()
5555
export class GridResizeByContentComponent implements OnInit {
5656
title = 'Example 30: Columns Resize by Content';
57-
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.`;
57+
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.`;
5858

5959
angularGrid!: AngularGridInstance;
6060
gridOptions!: GridOption;

src/app/examples/grid-tree-data-hierarchical.component.html

+4-4
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ <h2>
2626
<span>Expand All</span>
2727
</button>
2828
<button (click)="logFlatStructure()" class="btn btn-outline-secondary btn-sm">
29-
<span>Log Flag Structure</span>
29+
<span>Log Flat Structure</span>
3030
</button>
31-
<button (click)="logExpandedStructure()" class="btn btn-outline-secondary btn-sm">
32-
<span>Log Expanded Structure</span>
31+
<button (click)="logHierarchicalStructure()" class="btn btn-outline-secondary btn-sm">
32+
<span>Log Hierarchical Structure</span>
3333
</button>
3434
</div>
3535

@@ -54,4 +54,4 @@ <h2>
5454
[datasetHierarchical]="datasetHierarchical"
5555
(onAngularGridCreated)="angularGridReady($event)">
5656
</angular-slickgrid>
57-
</div>
57+
</div>

src/app/examples/grid-tree-data-hierarchical.component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ export class GridTreeDataHierarchicalComponent implements OnInit {
218218
this.angularGrid.treeDataService.toggleTreeDataCollapse(false);
219219
}
220220

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

src/app/examples/grid-tree-data-parent-child.component.html

+4-4
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ <h2>
2626
<span>Expand All</span>
2727
</button>
2828
<button (click)="logFlatStructure()" class="btn btn-outline-secondary btn-sm">
29-
<span>Log Flag Structure</span>
29+
<span>Log Flat Structure</span>
3030
</button>
31-
<button (click)="logExpandedStructure()" class="btn btn-outline-secondary btn-sm">
32-
<span>Log Expanded Structure</span>
31+
<button (click)="logHierarchicalStructure()" class="btn btn-outline-secondary btn-sm">
32+
<span>Log Hierarchical Structure</span>
3333
</button>
3434
</div>
3535
</div>
@@ -42,4 +42,4 @@ <h2>
4242
[dataset]="dataset"
4343
(onAngularGridCreated)="angularGridReady($event)">
4444
</angular-slickgrid>
45-
</div>
45+
</div>

src/app/examples/grid-tree-data-parent-child.component.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,16 @@ export class GridTreeDataParentChildComponent implements OnInit {
9898
enableTreeData: true, // you must enable this flag for the filtering & sorting to work as expected
9999
treeDataOptions: {
100100
columnId: 'title',
101-
levelPropName: 'indent',
102-
parentPropName: 'parentId'
101+
levelPropName: 'indent', // this is optional, except that in our case we just need to define it because we are adding new item in the demo
102+
parentPropName: 'parentId',
103+
104+
// you can optionally sort by a different column and/or sort direction
105+
initialSort: {
106+
columnId: 'title',
107+
direction: 'ASC'
108+
}
103109
},
110+
showCustomFooter: true,
104111
multiColumnSort: false, // multi-column sorting is not supported with Tree Data, so you need to disable it
105112
// change header/cell row height for material design theme
106113
headerRowHeight: 45,
@@ -166,7 +173,7 @@ export class GridTreeDataParentChildComponent implements OnInit {
166173
parentId: parentItemFound.id,
167174
title: `Task ${newId}`,
168175
duration: '1 day',
169-
percentComplete: Math.round(Math.random() * 100),
176+
percentComplete: 99,
170177
start: new Date(),
171178
finish: new Date(),
172179
effortDriven: false
@@ -176,11 +183,7 @@ export class GridTreeDataParentChildComponent implements OnInit {
176183
this.dataset = [...dataset]; // make a copy to trigger a dataset refresh
177184

178185
// add setTimeout to wait a full cycle because datasetChanged needs a full cycle
179-
// force a resort because of the tree data structure
180186
setTimeout(() => {
181-
const titleColumn = this.columnDefinitions.find(col => col.id === 'title') as Column;
182-
this.angularGrid.sortService.onLocalSortChanged(this.gridObj, [{ columnId: 'title', sortCol: titleColumn, sortAsc: true }]);
183-
184187
// scroll into the position, after insertion cycle, where the item was added
185188
const rowIndex = this.dataViewObj.getRowById(newItem.id);
186189
this.gridObj.scrollRowIntoView(rowIndex + 3);
@@ -195,7 +198,7 @@ export class GridTreeDataParentChildComponent implements OnInit {
195198
this.angularGrid.treeDataService.toggleTreeDataCollapse(false);
196199
}
197200

198-
logExpandedStructure() {
201+
logHierarchicalStructure() {
199202
console.log('exploded array', this.angularGrid.treeDataService.datasetHierarchical /* , JSON.stringify(explodedArray, null, 2) */);
200203
}
201204

@@ -232,8 +235,8 @@ export class GridTreeDataParentChildComponent implements OnInit {
232235
}
233236

234237
d['id'] = i;
235-
d['indent'] = indent;
236238
d['parentId'] = parentId;
239+
d['indent'] = indent;
237240
d['title'] = 'Task ' + i;
238241
d['duration'] = '5 days';
239242
d['percentComplete'] = Math.round(Math.random() * 100);

src/app/modules/angular-slickgrid/components/__tests__/angular-slickgrid-constructor.spec.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const filterServiceStub = {
9797
bindLocalOnSort: jest.fn(),
9898
bindBackendOnSort: jest.fn(),
9999
populateColumnFilterSearchTermPresets: jest.fn(),
100+
refreshTreeDataFilters: jest.fn(),
100101
getColumnFilters: jest.fn(),
101102
} as unknown as FilterService;
102103

@@ -150,10 +151,13 @@ const sortServiceStub = {
150151
dispose: jest.fn(),
151152
loadGridSorters: jest.fn(),
152153
processTreeDataInitialSort: jest.fn(),
154+
sortHierarchicalDataset: jest.fn(),
153155
} as unknown as SortService;
154156

155157
const treeDataServiceStub = {
158+
convertFlatDatasetConvertToHierarhicalView: jest.fn(),
156159
init: jest.fn(),
160+
initializeHierarchicalDataset: jest.fn(),
157161
dispose: jest.fn(),
158162
handleOnCellClick: jest.fn(),
159163
toggleTreeDataCollapse: jest.fn(),
@@ -1633,16 +1637,22 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () =
16331637
}
16341638
});
16351639

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

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

1645-
expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical);
1651+
setTimeout(() => {
1652+
expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical);
1653+
expect(refreshTreeSpy).not.toHaveBeenCalled();
1654+
done();
1655+
})
16461656
});
16471657

16481658
it('should change hierarchical dataset and expect processTreeDataInitialSort being called with other methods', (done) => {
@@ -1665,6 +1675,22 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () =
16651675
done();
16661676
}, 2);
16671677
});
1678+
1679+
it('should expect "refreshTreeDataFilters" method to be called when our flat dataset was already set and it just got changed a 2nd time', () => {
1680+
const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }];
1681+
const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }] }];
1682+
const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set');
1683+
jest.spyOn(treeDataServiceStub, 'initializeHierarchicalDataset').mockReturnValue({ hierarchical: mockHierarchical, flat: mockFlatDataset });
1684+
const refreshTreeSpy = jest.spyOn(filterServiceStub, 'refreshTreeDataFilters');
1685+
1686+
component.dataset = [{ id: 0, file: 'documents' }];
1687+
component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file', parentPropName: 'parentId', childrenPropName: 'files' } } as GridOption;
1688+
component.ngAfterViewInit();
1689+
component.dataset = mockFlatDataset;
1690+
1691+
expect(hierarchicalSpy).toHaveBeenCalledWith(mockHierarchical);
1692+
expect(refreshTreeSpy).toHaveBeenCalled();
1693+
});
16681694
});
16691695
});
16701696
});

src/app/modules/angular-slickgrid/components/__tests__/angular-slickgrid.component.spec.ts

+3-10
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,9 @@ import {
4343
RowMoveManagerExtension,
4444
RowSelectionExtension,
4545
} from '../../extensions';
46-
import * as utilities from '../../services/utilities';
4746

4847
declare const Slick: any;
4948

50-
function removeExtraSpaces(text: string) {
51-
return `${text}`.replace(/\s{2,}/g, '');
52-
}
53-
54-
const mockConvertParentChildArray = jest.fn();
55-
// @ts-ignore
56-
utilities.convertParentChildArrayToHierarchicalView = mockConvertParentChildArray;
57-
5849
const excelExportServiceStub = {
5950
init: jest.fn(),
6051
dispose: jest.fn(),
@@ -123,7 +114,9 @@ const sortServiceStub = {
123114
} as unknown as SortService;
124115

125116
const treeDataServiceStub = {
117+
convertFlatDatasetConvertToHierarhicalView: jest.fn(),
126118
init: jest.fn(),
119+
initializeHierarchicalDataset: jest.fn(),
127120
dispose: jest.fn(),
128121
handleOnCellClick: jest.fn(),
129122
toggleTreeDataCollapse: jest.fn(),
@@ -237,6 +230,7 @@ describe('App Component', () => {
237230
it('should convert parent/child dataset to hierarchical dataset when Tree Data is enabled and "onRowsChanged" was triggered', async (done) => {
238231
const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }];
239232
const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set');
233+
jest.spyOn(treeDataServiceStub, 'initializeHierarchicalDataset').mockReturnValue({ hierarchical: [], flat: mockFlatDataset });
240234

241235
component.gridId = 'grid1';
242236
component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file' } } as GridOption;
@@ -250,7 +244,6 @@ describe('App Component', () => {
250244

251245
setTimeout(() => {
252246
expect(hierarchicalSpy).toHaveBeenCalled();
253-
expect(mockConvertParentChildArray).toHaveBeenCalled();
254247
done();
255248
}, 51)
256249
});

src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts

+24-29
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import {
3535
Pagination,
3636
ServicePagination,
3737
SlickEventHandler,
38-
TreeDataOption,
3938
} from './../models/index';
4039
import { FilterFactory } from '../filters/filterFactory';
4140
import { autoAddEditorFormatterToColumnsWithEditor } from './slick-vanilla-utilities';
@@ -124,6 +123,7 @@ const slickgridEventPrefix = 'sg';
124123
export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnInit {
125124
private _dataset?: any[] | null;
126125
private _columnDefinitions!: Column[];
126+
private _currentDatasetLength = 0;
127127
private _eventHandler: SlickEventHandler = new Slick.EventHandler();
128128
private _angularGridInstances: AngularGridInstance | undefined;
129129
private _fixedHeight?: number | null;
@@ -213,9 +213,24 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
213213
get dataset(): any[] {
214214
return this.dataView.getItems();
215215
}
216-
set dataset(dataset: any[]) {
217-
this._dataset = dataset;
218-
this.refreshGridData(dataset);
216+
set dataset(newDataset: any[]) {
217+
const prevDatasetLn = this._currentDatasetLength;
218+
let data = newDataset;
219+
this._dataset = data;
220+
221+
// when Tree Data is enabled and we don't yet have the hierarchical dataset filled, we can force a convert & sort of the array
222+
if (this.gridOptions?.enableTreeData && Array.isArray(newDataset) && (newDataset.length > 0 || newDataset.length !== prevDatasetLn)) {
223+
const sortedDatasetResult = this.treeDataService.initializeHierarchicalDataset(data, this._columnDefinitions);
224+
this.sharedService.hierarchicalDataset = sortedDatasetResult.hierarchical;
225+
data = sortedDatasetResult.flat;
226+
227+
// if we add/remove item(s) from the dataset, we need to also refresh our tree data filters
228+
if (newDataset.length > 0 && prevDatasetLn > 0 && newDataset.length !== prevDatasetLn) {
229+
this.filterService.refreshTreeDataFilters();
230+
}
231+
}
232+
this.refreshGridData(data || []);
233+
this._currentDatasetLength = newDataset.length;
219234
}
220235

221236
@Input()
@@ -225,13 +240,13 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
225240
set datasetHierarchical(newHierarchicalDataset: any[] | undefined) {
226241
this.sharedService.hierarchicalDataset = newHierarchicalDataset;
227242

228-
if (newHierarchicalDataset && this.columnDefinitions && this.filterService && this.filterService.clearFilters) {
243+
if (newHierarchicalDataset && this.columnDefinitions && this.filterService?.clearFilters) {
229244
this.filterService.clearFilters();
230245
}
231246

232247
// when a hierarchical dataset is set afterward, we can reset the flat dataset and call a tree data sort that will overwrite the flat dataset
233248
setTimeout(() => {
234-
if (newHierarchicalDataset && this.dataView && this.sortService && this.sortService.processTreeDataInitialSort && this.gridOptions && this.gridOptions.enableTreeData) {
249+
if (newHierarchicalDataset && this.dataView && this.sortService?.processTreeDataInitialSort && this.gridOptions?.enableTreeData) {
235250
this.dataView.setItems([], this.gridOptions.datasetIdPropertyName);
236251
this.sortService.processTreeDataInitialSort();
237252
}
@@ -426,7 +441,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
426441

427442
if (Array.isArray(dataset) && this.grid && this.dataView && typeof this.dataView.setItems === 'function') {
428443
this.dataView.setItems(dataset, this.gridOptions.datasetIdPropertyName);
429-
if (!this.gridOptions.backendServiceApi) {
444+
if (!this.gridOptions.backendServiceApi && !this.gridOptions.enableTreeData) {
430445
this.dataView.reSort();
431446
}
432447

@@ -439,11 +454,6 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
439454
}
440455
}
441456
this._isDatasetInitialized = true;
442-
443-
// also update the hierarchical dataset
444-
if (dataset.length > 0 && this.gridOptions.treeDataOptions) {
445-
this.sharedService.hierarchicalDataset = this.treeDataSortComparer(dataset);
446-
}
447457
}
448458

449459
if (dataset) {
@@ -667,16 +677,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
667677
throw new Error('[Angular-Slickgrid] When enabling tree data, you must also provide the "treeDataOption" property in your Grid Options with "childrenPropName" or "parentPropName" (depending if your array is hierarchical or flat) for the Tree Data to work properly');
668678
}
669679

670-
this._eventHandler.subscribe(dataView.onRowsChanged, (e: any, args: any) => {
671-
// when dealing with Tree Data, anytime the flat dataset changes, we need to update our hierarchical dataset
672-
// this could be triggered by a DataView setItems or updateItem
673-
if (this.gridOptions && this.gridOptions.enableTreeData) {
674-
const items = this.dataView.getItems();
675-
if (Array.isArray(items) && items.length > 0 && !this._isDatasetInitialized) {
676-
this.sharedService.hierarchicalDataset = this.treeDataSortComparer(items);
677-
}
678-
}
679-
680+
this._eventHandler.subscribe(dataView.onRowsChanged, (_e: Event, args: any) => {
680681
// filtering data with local dataset will not always show correctly unless we call this updateRow/render
681682
// also don't use "invalidateRows" since it destroys the entire row and as bad user experience when updating a row
682683
// see commit: https://github.com/ghiscoding/Angular-Slickgrid/commit/bb62c0aa2314a5d61188ff005ccb564577f08805
@@ -805,6 +806,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
805806

806807
/** When data changes in the DataView, we'll refresh the metrics and/or display a warning if the dataset is empty */
807808
private handleOnItemCountChanged(currentPageRowItemCount: number, totalItemCount: number) {
809+
this._currentDatasetLength = totalItemCount;
808810
this.metrics = {
809811
startTime: new Date(),
810812
endTime: new Date(),
@@ -1218,13 +1220,6 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
12181220
}
12191221
}
12201222

1221-
private treeDataSortComparer(flatDataset: any[]): any[] {
1222-
const dataViewIdIdentifier = this.gridOptions && this.gridOptions.datasetIdPropertyName || 'id';
1223-
const treeDataOpt: TreeDataOption = this.gridOptions && this.gridOptions.treeDataOptions || { columnId: '' };
1224-
const treeDataOptions = { ...treeDataOpt, identifierPropName: treeDataOpt.identifierPropName || dataViewIdIdentifier };
1225-
return convertParentChildArrayToHierarchicalView(flatDataset, treeDataOptions);
1226-
}
1227-
12281223
/**
12291224
* For convenience to the user, we provide the property "editor" as an Angular-Slickgrid editor complex object
12301225
* however "editor" is used internally by SlickGrid for it's own Editor Factory

0 commit comments

Comments
 (0)