From e684d1af1c078a8861c3c94fe5486cbe68d57b85 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 30 Apr 2021 16:55:31 -0400 Subject: [PATCH] fix(tree): couple of issues found in Tree Data, fixes #307 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 --- .../src/examples/example05.html | 8 +- .../src/examples/example05.ts | 17 +++-- .../src/examples/example06.html | 8 +- .../src/examples/example06.ts | 4 +- .../common/src/formatters/treeFormatter.ts | 7 +- .../services/__tests__/sort.service.spec.ts | 3 + .../__tests__/treeData.service.spec.ts | 73 ++++++++++++++++++- packages/common/src/services/sort.service.ts | 33 +++++---- .../common/src/services/treeData.service.ts | 35 ++++++++- packages/common/src/services/utilities.ts | 12 +-- .../__tests__/slick-vanilla-grid.spec.ts | 26 +------ .../components/slick-vanilla-grid-bundle.ts | 44 ++++------- 12 files changed, 174 insertions(+), 96 deletions(-) diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example05.html b/examples/webpack-demo-vanilla-bundle/src/examples/example05.html index ef065f9b3..ace3b8862 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example05.html +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example05.html @@ -28,13 +28,13 @@
Expand All -
-
\ No newline at end of file + diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts index 2bdba9d4f..f44d0aed4 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts @@ -92,8 +92,13 @@ 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' + 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: { @@ -151,8 +156,8 @@ export class Example5 { 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 /* , JSON.stringify(explodedArray, null, 2) */); } logFlatStructure() { @@ -188,9 +193,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); diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example06.html b/examples/webpack-demo-vanilla-bundle/src/examples/example06.html index 61b282ab7..4530c4d9f 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example06.html +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example06.html @@ -27,10 +27,10 @@
Expand All -
@@ -58,4 +58,4 @@
-
\ No newline at end of file + diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example06.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example06.ts index bd2a7679e..201d91b8e 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example06.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example06.ts @@ -172,8 +172,8 @@ 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 /* , JSON.stringify(explodedArray, null, 2) */); } logFlatStructure() { diff --git a/packages/common/src/formatters/treeFormatter.ts b/packages/common/src/formatters/treeFormatter.ts index dca87a767..c0eb46341 100644 --- a/packages/common/src/formatters/treeFormatter.ts +++ b/packages/common/src/formatters/treeFormatter.ts @@ -26,16 +26,17 @@ export const treeFormatter: Formatter = (_row, _cell, value, columnDef, dataCont throw new Error('You must provide valid "treeDataOptions" in your Grid Options and it seems that there are no tree level found in this row'); } - if (dataView && dataView.getIdxById && dataView.getItemByIdx) { + if (dataView?.getItemByIdx) { if (typeof outputValue === 'string') { outputValue = htmlEncode(outputValue); } const identifierPropName = dataView.getIdPropertyName() || 'id'; - const spacer = ``; + const treeLevel = dataContext[treeLevelPropName] || 0; + const spacer = ``; const idx = dataView.getIdxById(dataContext[identifierPropName]); const nextItemRow = dataView.getItemByIdx((idx || 0) + 1); - if (nextItemRow && nextItemRow[treeLevelPropName] > dataContext[treeLevelPropName]) { + if (nextItemRow?.[treeLevelPropName] > treeLevel) { if (dataContext.__collapsed) { return `${spacer} ${outputValue}`; } else { diff --git a/packages/common/src/services/__tests__/sort.service.spec.ts b/packages/common/src/services/__tests__/sort.service.spec.ts index 6d41498d0..2e1f05e6d 100644 --- a/packages/common/src/services/__tests__/sort.service.spec.ts +++ b/packages/common/src/services/__tests__/sort.service.spec.ts @@ -989,6 +989,7 @@ describe('SortService', () => { { columnId: 'file', sortAsc: false, sortCol: { id: 'file', field: 'file', width: 75 } } ]; + sharedService.hierarchicalDataset = []; service.bindLocalOnSort(gridStub); gridStub.onSort.notify({ multiColumnSort: true, sortCols: mockSortedCols, grid: gridStub }, new Slick.EventData(), gridStub); @@ -1009,11 +1010,13 @@ describe('SortService', () => { const spyCurrentSort = jest.spyOn(service, 'getCurrentLocalSorters'); const spyOnLocalSort = jest.spyOn(service, 'onLocalSortChanged'); const spyUpdateSorting = jest.spyOn(service, 'updateSorting'); + const mockSortedCols: ColumnSort[] = [ { columnId: 'lastName', sortAsc: true, sortCol: { id: 'lastName', field: 'lastName', width: 100 } }, { columnId: 'file', sortAsc: false, sortCol: { id: 'file', field: 'file', width: 75 } } ]; + sharedService.hierarchicalDataset = []; service.bindLocalOnSort(gridStub); gridStub.onSort.notify({ multiColumnSort: true, sortCols: mockSortedCols, grid: gridStub }, new Slick.EventData(), gridStub); diff --git a/packages/common/src/services/__tests__/treeData.service.spec.ts b/packages/common/src/services/__tests__/treeData.service.spec.ts index caa6d8278..0a5b5093b 100644 --- a/packages/common/src/services/__tests__/treeData.service.spec.ts +++ b/packages/common/src/services/__tests__/treeData.service.spec.ts @@ -1,7 +1,11 @@ + import { Column, SlickDataView, GridOption, SlickEventHandler, SlickGrid, SlickNamespace } from '../../interfaces/index'; import { SharedService } from '../shared.service'; +import { SortService } from '../sort.service'; import { TreeDataService } from '../treeData.service'; +import * as utilities from '../utilities'; +const mockConvertParentChildArray = jest.fn(); declare const Slick: SlickNamespace; const gridOptionsMock = { @@ -35,13 +39,18 @@ const gridStub = { setSortColumns: jest.fn(), } as unknown as SlickGrid; +const sortServiceStub = { + clearSorting: jest.fn(), + sortHierarchicalDataset: jest.fn(), +} as unknown as SortService; + describe('TreeData Service', () => { let service: TreeDataService; let slickgridEventHandler: SlickEventHandler; const sharedService = new SharedService(); beforeEach(() => { - service = new TreeDataService(sharedService); + service = new TreeDataService(sharedService, sortServiceStub); slickgridEventHandler = service.eventHandler; jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub); }); @@ -160,9 +169,7 @@ describe('TreeData Service', () => { beforeEach(() => { itemsMock = [{ file: 'myFile.txt', size: 0.5 }, { file: 'myMusic.txt', size: 5.3 }]; - gridOptionsMock.treeDataOptions = { - columnId: 'file' - }; + gridOptionsMock.treeDataOptions = { columnId: 'file' }; jest.clearAllMocks(); }); @@ -209,5 +216,63 @@ describe('TreeData Service', () => { ]); }); }); + + describe('initializeHierarchicalDataset method', () => { + let mockColumns: Column[]; + let mockFlatDataset; + + beforeEach(() => { + mockColumns = [{ id: 'file', field: 'file', }, { id: 'size', field: 'size', }] as Column[]; + mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', size: 1.2, parentId: 0 }, { id: 2, file: 'todo.txt', size: 2.3, parentId: 0 }]; + gridOptionsMock.treeDataOptions = { columnId: 'file', parentPropName: 'parentId' }; + jest.clearAllMocks(); + }); + + it('should sort by the Tree column when there is no initial sort provided', () => { + const mockHierarchical = [{ + id: 0, + file: 'documents', + files: [{ id: 2, file: 'todo.txt', size: 2.3, }, { id: 1, file: 'vacation.txt', size: 1.2, }] + }]; + const setSortSpy = jest.spyOn(gridStub, 'setSortColumns'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(0); + jest.spyOn(sortServiceStub, 'sortHierarchicalDataset').mockReturnValue({ flat: mockFlatDataset, hierarchical: mockHierarchical }); + + service.init(gridStub); + const result = service.initializeHierarchicalDataset(mockFlatDataset, [mockColumn]); + + expect(setSortSpy).toHaveBeenCalledWith([{ + columnId: 'file', + sortAsc: true, + sortCol: mockColumn + }]); + expect(result).toEqual({ flat: mockFlatDataset, hierarchical: mockHierarchical }); + }); + + it('should sort by the Tree column by the "initialSort" provided', () => { + gridOptionsMock.treeDataOptions.initialSort = { + columnId: 'size', + direction: 'desc' + }; + const mockHierarchical = [{ + id: 0, + file: 'documents', + files: [{ id: 1, file: 'vacation.txt', size: 1.2, }, { id: 2, file: 'todo.txt', size: 2.3, }] + }]; + const setSortSpy = jest.spyOn(gridStub, 'setSortColumns'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(0); + jest.spyOn(sortServiceStub, 'sortHierarchicalDataset').mockReturnValue({ flat: mockFlatDataset, hierarchical: mockHierarchical }); + + service.init(gridStub); + const result = service.initializeHierarchicalDataset(mockFlatDataset, [mockColumn]); + + expect(setSortSpy).toHaveBeenCalledWith([{ + columnId: 'size', + sortAsc: false, + sortCol: mockColumn + }]); + expect(result).toEqual({ flat: mockFlatDataset, hierarchical: mockHierarchical }); + }); + }); }); }); diff --git a/packages/common/src/services/sort.service.ts b/packages/common/src/services/sort.service.ts index f41b0c595..75532dcf3 100644 --- a/packages/common/src/services/sort.service.ts +++ b/packages/common/src/services/sort.service.ts @@ -323,16 +323,16 @@ export class SortService { /** Process the initial sort, typically it will sort ascending by the column that has the Tree Data unless user specifies a different initialSort */ processTreeDataInitialSort() { // when a Tree Data view is defined, we must sort the data so that the UI works correctly - if (this._gridOptions && this._gridOptions.enableTreeData && this._gridOptions.treeDataOptions) { + if (this._gridOptions?.enableTreeData && this._gridOptions.treeDataOptions) { // first presort it once by tree level const treeDataOptions = this._gridOptions.treeDataOptions; - const columnWithTreeData = this._columnDefinitions.find((col: Column) => col && col.id === treeDataOptions.columnId); + const columnWithTreeData = this._columnDefinitions.find((col: Column) => col.id === treeDataOptions.columnId); if (columnWithTreeData) { let sortDirection = SortDirection.ASC; let sortTreeLevelColumn: ColumnSort = { columnId: treeDataOptions.columnId, sortCol: columnWithTreeData, sortAsc: true }; // user could provide a custom sort field id, if so get that column and sort by it - if (treeDataOptions && treeDataOptions.initialSort && treeDataOptions.initialSort.columnId) { + if (treeDataOptions?.initialSort?.columnId) { const initialSortColumnId = treeDataOptions.initialSort.columnId; const initialSortColumn = this._columnDefinitions.find((col: Column) => col.id === initialSortColumnId); sortDirection = (treeDataOptions.initialSort.direction || SortDirection.ASC).toUpperCase() as SortDirection; @@ -340,7 +340,7 @@ export class SortService { } // when we have a valid column with Tree Data, we can sort by that column - if (sortTreeLevelColumn && sortTreeLevelColumn.columnId) { + if (sortTreeLevelColumn?.columnId && this.sharedService?.hierarchicalDataset) { this.updateSorting([{ columnId: sortTreeLevelColumn.columnId || '', direction: sortDirection }]); } } @@ -383,12 +383,8 @@ export class SortService { if (isTreeDataEnabled && this.sharedService && Array.isArray(this.sharedService.hierarchicalDataset)) { const hierarchicalDataset = this.sharedService.hierarchicalDataset; - this.sortTreeData(hierarchicalDataset, sortColumns); - const dataViewIdIdentifier = this._gridOptions?.datasetIdPropertyName ?? 'id'; - const treeDataOpt: TreeDataOption = this._gridOptions?.treeDataOptions ?? { columnId: '' }; - const treeDataOptions = { ...treeDataOpt, identifierPropName: treeDataOpt.identifierPropName ?? dataViewIdIdentifier }; - const sortedFlatArray = convertHierarchicalViewToParentChildArray(hierarchicalDataset, treeDataOptions); - dataView.setItems(sortedFlatArray, this._gridOptions?.datasetIdPropertyName ?? 'id'); + const datasetSortResult = this.sortHierarchicalDataset(hierarchicalDataset, sortColumns); + this._dataView.setItems(datasetSortResult.flat, this._gridOptions?.datasetIdPropertyName ?? 'id'); } else { dataView.sort(this.sortComparers.bind(this, sortColumns)); } @@ -406,6 +402,17 @@ export class SortService { } } + /** Takes a hierarchical dataset and sort it recursively, */ + sortHierarchicalDataset(hierarchicalDataset: any[], sortColumns: Array): { hierarchical: any[]; flat: any[]; } { + this.sortTreeData(hierarchicalDataset, sortColumns); + const dataViewIdIdentifier = this._gridOptions?.datasetIdPropertyName ?? 'id'; + const treeDataOpt: TreeDataOption = this._gridOptions?.treeDataOptions ?? { columnId: '' }; + const treeDataOptions = { ...treeDataOpt, identifierPropName: treeDataOpt.identifierPropName ?? dataViewIdIdentifier }; + const sortedFlatArray = convertHierarchicalViewToParentChildArray(hierarchicalDataset, treeDataOptions); + + return { hierarchical: hierarchicalDataset, flat: sortedFlatArray }; + } + /** Call a local grid sort by its default sort field id (user can customize default field by configuring "defaultColumnSortFieldId" in the grid options, defaults to "id") */ sortLocalGridByDefaultSortFieldId() { const sortColFieldId = this._gridOptions && this._gridOptions.defaultColumnSortFieldId || this._gridOptions.datasetIdPropertyName || 'id'; @@ -510,11 +517,11 @@ export class SortService { } if (Array.isArray(sorters)) { - const backendApi = this._gridOptions && this._gridOptions.backendServiceApi; + const backendApi = this._gridOptions?.backendServiceApi; if (backendApi) { - const backendApiService = backendApi && backendApi.service; - if (backendApiService && backendApiService.updateSorters) { + const backendApiService = backendApi?.service; + if (backendApiService?.updateSorters) { backendApiService.updateSorters(undefined, sorters); if (triggerBackendQuery) { this.backendUtilities?.refreshBackendDataset(this._gridOptions); diff --git a/packages/common/src/services/treeData.service.ts b/packages/common/src/services/treeData.service.ts index c6061f89a..77e58c9ef 100644 --- a/packages/common/src/services/treeData.service.ts +++ b/packages/common/src/services/treeData.service.ts @@ -1,5 +1,7 @@ -import { SlickDataView, GridOption, SlickEventHandler, SlickGrid, SlickNamespace, GetSlickEventType, OnClickEventArgs, SlickEventData } from '../interfaces/index'; +import { Column, ColumnSort, GetSlickEventType, GridOption, OnClickEventArgs, SlickDataView, SlickEventData, SlickEventHandler, SlickGrid, SlickNamespace, TreeDataOption, } from '../interfaces/index'; +import { convertParentChildArrayToHierarchicalView } from './utilities'; import { SharedService } from './shared.service'; +import { SortService } from './sort.service'; // using external non-typed js libraries declare const Slick: SlickNamespace; @@ -8,7 +10,7 @@ export class TreeDataService { private _grid!: SlickGrid; private _eventHandler: SlickEventHandler; - constructor(private sharedService: SharedService) { + constructor(private sharedService: SharedService, private sortService: SortService) { this._eventHandler = new Slick.EventHandler(); } @@ -51,6 +53,35 @@ export class TreeDataService { } } + /** Takes a flat dataset, converts it into a hierarchical dataset, sort it by recursion and finally return back the final and sorted flat array */ + initializeHierarchicalDataset(flatDataset: any[], columnDefinitions: Column[]) { + // 1- convert the flat array into a hierarchical array + const datasetHierarchical = this.convertFlatDatasetConvertToHierarhicalView(flatDataset); + + // 2- sort the hierarchical array recursively by an optional "initialSort" OR if nothing is provided we'll sort by the column defined as the Tree column + // also note that multi-column is not currently supported with Tree Data + const treeDataOptions = this.gridOptions?.treeDataOptions; + const initialColumnSort = treeDataOptions?.initialSort ?? { columnId: treeDataOptions?.columnId ?? '', direction: 'ASC' }; + const columnSort: ColumnSort = { + columnId: initialColumnSort.columnId, + sortAsc: initialColumnSort?.direction?.toUpperCase() !== 'DESC', + sortCol: columnDefinitions[this._grid.getColumnIndex(initialColumnSort.columnId || '')], + }; + const datasetSortResult = this.sortService.sortHierarchicalDataset(datasetHierarchical, [columnSort]); + + // and finally add the sorting icon (this has to be done manually in SlickGrid) to the column we used for the sorting + this._grid.setSortColumns([columnSort]); + + return datasetSortResult; + } + + convertFlatDatasetConvertToHierarhicalView(flatDataset: any[]): any[] { + const dataViewIdIdentifier = this.gridOptions?.datasetIdPropertyName ?? 'id'; + const treeDataOpt: TreeDataOption = this.gridOptions?.treeDataOptions ?? { columnId: 'id' }; + const treeDataOptions = { ...treeDataOpt, identifierPropName: treeDataOpt.identifierPropName ?? dataViewIdIdentifier }; + return convertParentChildArrayToHierarchicalView(flatDataset, treeDataOptions); + } + handleOnCellClick(event: SlickEventData, args: OnClickEventArgs) { if (event && args) { const targetElm: any = event.target || {}; diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts index 70f1b4bde..f94f2549a 100644 --- a/packages/common/src/services/utilities.ts +++ b/packages/common/src/services/utilities.ts @@ -83,23 +83,23 @@ export function convertParentChildArrayToHierarchicalView(flatArray: T[ const identifierPropName = options?.identifierPropName ?? 'id'; const hasChildrenFlagPropName = '__hasChildren'; const treeLevelPropName = '__treeLevel'; - const inputArray: T[] = $.extend(true, [], flatArray); + const inputArray: T[] = deepCopy(flatArray || []); const roots: T[] = []; // things without parent // make them accessible by guid on this map - const all = {}; + const all: any = {}; - inputArray.forEach((item) => (all as any)[(item as any)[identifierPropName]] = item); + inputArray.forEach((item: any) => all[item[identifierPropName]] = item); // connect childrens to its parent, and split roots apart Object.keys(all).forEach((id) => { - const item = (all as any)[id]; - if (item[parentPropName] === null || !item.hasOwnProperty(parentPropName)) { + const item = all[id]; + if (!(parentPropName in item) || item[parentPropName] === null || item[parentPropName] === undefined || item[parentPropName] === '') { delete item[parentPropName]; roots.push(item); } else if (item[parentPropName] in all) { - const p = (all as any)[item[parentPropName]]; + const p = all[item[parentPropName]]; if (!(childrenPropName in p)) { p[childrenPropName] = []; } diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts index 7597846d8..6b93400a7 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts @@ -43,7 +43,6 @@ import { } from '@slickgrid-universal/common'; import { GraphqlService, GraphqlPaginatedResult, GraphqlServiceApi, GraphqlServiceOption } from '@slickgrid-universal/graphql'; import { SlickCompositeEditorComponent } from '@slickgrid-universal/composite-editor-component'; -import * as utilities from '@slickgrid-universal/common/dist/commonjs/services/utilities'; import * as slickVanillaUtilities from '../slick-vanilla-utilities'; import { SlickVanillaGridBundle } from '../slick-vanilla-grid-bundle'; @@ -57,7 +56,6 @@ import { UniversalContainerService } from '../../services/universalContainer.ser import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub'; jest.mock('../../services/textExport.service'); -const mockConvertParentChildArray = jest.fn(); const mockAutoAddCustomEditorFormatter = jest.fn(); (slickVanillaUtilities.autoAddEditorFormatterToColumnsWithEditor as any) = mockAutoAddCustomEditorFormatter; @@ -175,10 +173,13 @@ const sortServiceStub = { dispose: jest.fn(), loadGridSorters: jest.fn(), processTreeDataInitialSort: jest.fn(), + sortHierarchicalDataset: jest.fn(), } as unknown as SortService; const treeDataServiceStub = { init: jest.fn(), + convertFlatDatasetConvertToHierarhicalView: jest.fn(), + initializeHierarchicalDataset: jest.fn(), dispose: jest.fn(), handleOnCellClick: jest.fn(), toggleTreeDataCollapse: jest.fn(), @@ -2048,6 +2049,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () 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, 'initializeHierarchicalDataset').mockReturnValue({ hierarchical: mockHierarchical, flat: mockFlatDataset }); component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file', parentPropName: 'parentId', childrenPropName: 'files' } } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); @@ -2072,34 +2074,14 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () expect(processSpy).toHaveBeenCalled(); expect(setItemsSpy).toHaveBeenCalledWith([], 'id'); }); - - it('should convert parent/child dataset to hierarchical dataset when Tree Data is enabled and "onRowsChanged" was triggered', () => { - // @ts-ignore:2540 - utilities.convertParentChildArrayToHierarchicalView = mockConvertParentChildArray; - - const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }]; - const hierarchicalSpy = jest.spyOn(SharedService.prototype, 'hierarchicalDataset', 'set'); - jest.spyOn(mockDataView, 'getItems').mockReturnValue(mockFlatDataset); - - component.gridOptions = { enableTreeData: true, treeDataOptions: { columnId: 'file' } }; - component.initialization(divContainer, slickEventHandler); - component.dataset = mockFlatDataset; - component.isDatasetInitialized = false; - mockDataView.onRowsChanged.notify({ itemCount: 0, dataView: mockDataView, rows: [1, 2, 3], calledOnRowCountChanged: false }); - - expect(hierarchicalSpy).toHaveBeenCalled(); - expect(mockConvertParentChildArray).toHaveBeenCalled(); - }); }); }); }); describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor with a Hierarchical Dataset', () => { - // jest.mock('slickgrid/slick.core', () => mockSlickCoreImplementation); jest.mock('slickgrid/slick.grid', () => mockGridImplementation); jest.mock('slickgrid/plugins/slick.draggablegrouping', () => mockDraggableGroupingImplementation); Slick.Grid = mockGridImplementation; - // Slick.EventHandler = mockSlickCoreImplementation; Slick.Data = { DataView: mockDataViewImplementation, GroupItemMetadataProvider: mockGroupItemMetaProviderImplementation }; Slick.DraggableGrouping = mockDraggableGroupingImplementation; diff --git a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts index 65e04bf74..41b889131 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -29,7 +29,6 @@ import { SlickGroupItemMetadataProvider, SlickNamespace, Subscription, - TreeDataOption, // extensions AutoTooltipExtension, @@ -68,7 +67,6 @@ import { TreeDataService, // utilities - convertParentChildArrayToHierarchicalView, emptyElement, GetSlickEventType, } from '@slickgrid-universal/common'; @@ -171,7 +169,15 @@ export class SlickVanillaGridBundle { set dataset(newDataset: any[]) { const prevDatasetLn = this.dataView.getLength(); const isDeepCopyDataOnPageLoadEnabled = !!(this._gridOptions?.enableDeepCopyDatasetOnPageLoad); - const data = isDeepCopyDataOnPageLoadEnabled ? $.extend(true, [], newDataset) : newDataset; + let data = isDeepCopyDataOnPageLoadEnabled ? $.extend(true, [], newDataset) : newDataset; + + // when Tree Data is enabled and we don't yet have the hierarchical dataset filled, we can force a convert & sort of the array + if (this._gridOptions.enableTreeData && Array.isArray(newDataset) && newDataset.length > 0) { + const sortedDatasetResult = this.treeDataService.initializeHierarchicalDataset(data, this._columnDefinitions); + this.sharedService.hierarchicalDataset = sortedDatasetResult.hierarchical; + data = sortedDatasetResult.flat; + } + this.refreshGridData(data || []); // expand/autofit columns on first page load @@ -188,12 +194,12 @@ export class SlickVanillaGridBundle { set datasetHierarchical(newHierarchicalDataset: any[] | undefined) { this.sharedService.hierarchicalDataset = newHierarchicalDataset; - if (newHierarchicalDataset && this.columnDefinitions && this.filterService && this.filterService.clearFilters) { + if (newHierarchicalDataset && this.columnDefinitions && this.filterService?.clearFilters) { this.filterService.clearFilters(); } // 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 - if (newHierarchicalDataset && this.sortService && this.sortService.processTreeDataInitialSort) { + if (newHierarchicalDataset && this.sortService?.processTreeDataInitialSort) { this.dataView.setItems([], this._gridOptions.datasetIdPropertyName); this.sortService.processTreeDataInitialSort(); } @@ -334,7 +340,7 @@ export class SlickVanillaGridBundle { this.filterService = services?.filterService ?? new FilterService(this.filterFactory, this._eventPubSubService, this.sharedService, this.backendUtilityService); this.resizerService = services?.resizerService ?? new ResizerService(this._eventPubSubService); this.sortService = services?.sortService ?? new SortService(this.sharedService, this._eventPubSubService, this.backendUtilityService); - this.treeDataService = services?.treeDataService ?? new TreeDataService(this.sharedService); + this.treeDataService = services?.treeDataService ?? new TreeDataService(this.sharedService, this.sortService); this.paginationService = services?.paginationService ?? new PaginationService(this._eventPubSubService, this.sharedService, this.backendUtilityService); // extensions @@ -565,16 +571,6 @@ export class SlickVanillaGridBundle { if (!this._gridOptions.treeDataOptions || !this._gridOptions.treeDataOptions.columnId) { throw new Error('[Slickgrid-Universal] 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'); } - - // anytime the flat dataset changes, we need to update our hierarchical dataset - // this could be triggered by a DataView setItems or updateItem - const onRowsChangedHandler = this.dataView.onRowsChanged; - (this._eventHandler as SlickEventHandler>).subscribe(onRowsChangedHandler, () => { - const items = this.dataView.getItems(); - if (Array.isArray(items) && items.length > 0 && !this._isDatasetInitialized) { - this.sharedService.hierarchicalDataset = this.treeDataSortComparer(items); - } - }); } // if you don't want the items that are not visible (due to being filtered out or being on a different page) @@ -1016,9 +1012,9 @@ export class SlickVanillaGridBundle { this.displayEmptyDataWarning(finalTotalCount < 1); } - if (Array.isArray(dataset) && this.slickGrid && this.dataView && typeof this.dataView.setItems === 'function') { + if (Array.isArray(dataset) && this.slickGrid && this.dataView?.setItems) { this.dataView.setItems(dataset, this._gridOptions.datasetIdPropertyName); - if (!this._gridOptions.backendServiceApi) { + if (!this._gridOptions.backendServiceApi && !this._gridOptions.enableTreeData) { this.dataView.reSort(); } @@ -1031,11 +1027,6 @@ export class SlickVanillaGridBundle { } } this._isDatasetInitialized = true; - - // also update the hierarchical dataset - if (dataset.length > 0 && this._gridOptions.treeDataOptions) { - this.sharedService.hierarchicalDataset = this.treeDataSortComparer(dataset); - } } if (dataset) { @@ -1408,13 +1399,6 @@ export class SlickVanillaGridBundle { }); } - private treeDataSortComparer(flatDataset: any[]): any[] { - const dataViewIdIdentifier = this._gridOptions?.datasetIdPropertyName ?? 'id'; - const treeDataOpt: TreeDataOption = this._gridOptions?.treeDataOptions ?? { columnId: '' }; - const treeDataOptions = { ...treeDataOpt, identifierPropName: treeDataOpt.identifierPropName ?? dataViewIdIdentifier }; - return convertParentChildArrayToHierarchicalView(flatDataset, treeDataOptions); - } - /** Translate all Custom Footer Texts (footer with metrics) */ private translateCustomFooterTexts() { if (this.slickFooter && this.translaterService?.translate) {