Skip to content

Commit

Permalink
fix: addItem from grid service should work with tree data
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed May 4, 2021
1 parent 7c39193 commit 8b468f0
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class Example5 {
enableTreeData: true, // you must enable this flag for the filtering & sorting to work as expected
treeDataOptions: {
columnId: 'title',
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
// 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
Expand All @@ -109,23 +109,21 @@ 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.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.sgb.dataset.find((item) => item[treeLevelPropName] === newTreeLevel);
const parentItemFound = this.sgb.dataView.getItemByIdx(childItemFound[parentPropName]);

if (childItemFound && parentItemFound) {
const newItem = {
id: newId,
indent: newTreeLevel,
parentId: parentItemFound.id,
title: `Task ${newId}`,
duration: '1 day',
Expand Down
13 changes: 8 additions & 5 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 Down
71 changes: 64 additions & 7 deletions packages/common/src/services/__tests__/grid.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'jest-extended';

import { FilterService, GridService, GridStateService, PaginationService, PubSubService, SharedService, SortService } from '../index';
import { FilterService, GridService, GridStateService, PaginationService, PubSubService, SharedService, SortService, TreeDataService } from '../index';
import { GridOption, CellArgs, Column, OnEventArgs, SlickGrid, SlickDataView, SlickNamespace } from '../../interfaces/index';

jest.useFakeTimers();
Expand All @@ -19,13 +19,9 @@ Slick.RowSelectionModel = mockSelectionModelImplementation;

const filterServiceStub = {
clearFilters: jest.fn(),
refreshTreeDataFilters: jest.fn(),
} as unknown as FilterService;

// const gridStateServiceStub = {
// needToPreserveRowSelection: jest.fn(),
// resetColumns: jest.fn(),
// } as unknown as GridStateService;

const pubSubServiceStub = {
publish: jest.fn(),
subscribe: jest.fn(),
Expand All @@ -47,10 +43,12 @@ const dataviewStub = {
getIdxById: jest.fn(),
getItemMetadata: jest.fn(),
getItem: jest.fn(),
getItems: jest.fn(),
getRowById: jest.fn(),
insertItem: jest.fn(),
insertItems: jest.fn(),
reSort: jest.fn(),
setItems: jest.fn(),
updateItem: jest.fn(),
updateItems: jest.fn(),
} as unknown as SlickDataView;
Expand Down Expand Up @@ -87,6 +85,15 @@ const paginationServiceStub = {
goToLastPage: jest.fn(),
} as unknown as PaginationService;

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

describe('Grid Service', () => {
let service: GridService;
const sharedService = new SharedService();
Expand All @@ -95,7 +102,7 @@ describe('Grid Service', () => {
jest.spyOn(gridStub, 'getOptions').mockReturnValue(mockGridOptions);

beforeEach(() => {
service = new GridService(gridStateServiceStub, filterServiceStub, pubSubServiceStub, paginationServiceStub, sharedService, sortServiceStub);
service = new GridService(gridStateServiceStub, filterServiceStub, pubSubServiceStub, paginationServiceStub, sharedService, sortServiceStub, treeDataServiceStub);
service.init(gridStub);
});

Expand Down Expand Up @@ -796,6 +803,56 @@ describe('Grid Service', () => {
jest.spyOn(gridStub, 'getOptions').mockReturnValue(mockGridOptions);
});

it('should invalidate and rerender the tree dataset when grid option "enableTreeData" is set when calling "addItem"', () => {
const mockItem = { id: 3, file: 'blah.txt', size: 2, parentId: 0 };
const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }, mockItem];
const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }, mockItem] }];
const mockColumns = [{ id: 'file', field: 'file', }, { id: 'size', field: 'size', }] as Column[];

jest.spyOn(dataviewStub, 'getItems').mockReturnValue(mockFlatDataset);
jest.spyOn(dataviewStub, 'getRowById').mockReturnValue(0);
jest.spyOn(treeDataServiceStub, 'convertToHierarchicalDatasetAndSort').mockReturnValue({ flat: mockFlatDataset, hierarchical: mockHierarchical });
jest.spyOn(gridStub, 'getOptions').mockReturnValue({ enableAutoResize: true, enableRowSelection: true, enableTreeData: true } as GridOption);
jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(mockColumns);
const setItemSpy = jest.spyOn(dataviewStub, 'setItems');
const addSpy = jest.spyOn(dataviewStub, 'addItem');
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');
const invalidateSpy = jest.spyOn(service, 'invalidateHierarchicalDataset');

service.addItem(mockItem);

expect(addSpy).toHaveBeenCalledTimes(1);
expect(addSpy).toHaveBeenCalledWith(mockItem);
expect(pubSubSpy).toHaveBeenLastCalledWith(`onItemAdded`, mockItem);
expect(invalidateSpy).toHaveBeenCalled();
expect(setItemSpy).toHaveBeenCalledWith(mockFlatDataset);
});

it('should invalidate and rerender the tree dataset when grid option "enableTreeData" is set when calling "addItems"', () => {
const mockItem = { id: 3, file: 'blah.txt', size: 2, parentId: 0 };
const mockFlatDataset = [{ id: 0, file: 'documents' }, { id: 1, file: 'vacation.txt', parentId: 0 }, mockItem];
const mockHierarchical = [{ id: 0, file: 'documents', files: [{ id: 1, file: 'vacation.txt' }, mockItem] }];
const mockColumns = [{ id: 'file', field: 'file', }, { id: 'size', field: 'size', }] as Column[];

jest.spyOn(dataviewStub, 'getItems').mockReturnValue(mockFlatDataset);
jest.spyOn(dataviewStub, 'getRowById').mockReturnValue(0);
jest.spyOn(treeDataServiceStub, 'convertToHierarchicalDatasetAndSort').mockReturnValue({ flat: mockFlatDataset, hierarchical: mockHierarchical });
jest.spyOn(gridStub, 'getOptions').mockReturnValue({ enableAutoResize: true, enableRowSelection: true, enableTreeData: true } as GridOption);
jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(mockColumns);
const setItemSpy = jest.spyOn(dataviewStub, 'setItems');
const addSpy = jest.spyOn(dataviewStub, 'addItems');
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');
const invalidateSpy = jest.spyOn(service, 'invalidateHierarchicalDataset');

service.addItems([mockItem]);

expect(addSpy).toHaveBeenCalledTimes(1);
expect(addSpy).toHaveBeenCalledWith([mockItem]);
expect(pubSubSpy).toHaveBeenLastCalledWith(`onItemAdded`, [mockItem]);
expect(invalidateSpy).toHaveBeenCalled();
expect(setItemSpy).toHaveBeenCalledWith(mockFlatDataset);
});

it('should throw an error when 1st argument for the item object is missing the Id defined by the "datasetIdPropertyName" property', () => {
jest.spyOn(gridStub, 'getOptions').mockReturnValue({ enableAutoResize: true, datasetIdPropertyName: 'customId' } as GridOption);
expect(() => service.addItem(null as any)).toThrowError('Adding an item requires the item to include an "customId" property');
Expand Down
57 changes: 51 additions & 6 deletions packages/common/src/services/__tests__/treeData.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ describe('TreeData Service', () => {

beforeEach(() => {
gridOptionsMock.backendServiceApi = undefined;
gridOptionsMock.enablePagination = false;
gridOptionsMock.multiColumnSort = false;
gridOptionsMock.treeDataOptions = {
columnId: 'file'
};
service = new TreeDataService(sharedService, sortServiceStub);
slickgridEventHandler = service.eventHandler;
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
Expand All @@ -78,7 +82,17 @@ describe('TreeData Service', () => {
gridOptionsMock.multiColumnSort = true;
service.init(gridStub);
} catch (e) {
expect(e.toString()).toContain('[Slickgrid-Universal] Tree Data does not currently support multi-column sorting');
expect(e.toString()).toContain('[Slickgrid-Universal] It looks like you are trying to use Tree Data with multi-column sorting');
done();
}
});

it('should throw an error when enableTreeData is enabled with Pagination since that is not supported', (done) => {
try {
gridOptionsMock.enablePagination = true;
service.init(gridStub);
} catch (e) {
expect(e.toString()).toContain('[Slickgrid-Universal] It looks like you are trying to use Tree Data with Pagination and/or a Backend Service (OData, GraphQL) but unfortunately that is simply not supported because of its complexity.');
done();
}
});
Expand All @@ -92,7 +106,17 @@ describe('TreeData Service', () => {
};
service.init(gridStub);
} catch (e) {
expect(e.toString()).toContain('[Slickgrid-Universal] Tree Data does not support backend services (like OData, GraphQL) and/or Pagination');
expect(e.toString()).toContain('[Slickgrid-Universal] It looks like you are trying to use Tree Data with Pagination and/or a Backend Service (OData, GraphQL) but unfortunately that is simply not supported because of its complexity.');
done();
}
});

it('should throw an error when enableTreeData is enabled without passing a "columnId"', (done) => {
try {
gridOptionsMock.treeDataOptions = {} as any;
service.init(gridStub);
} catch (e) {
expect(e.toString()).toContain('[Slickgrid-Universal] When enabling tree data, you must also provide the "treeDataOption" property in your Grid Options with "childrenPropName" or "parentPropName"');
done();
}
});
Expand Down Expand Up @@ -272,12 +296,12 @@ describe('TreeData Service', () => {
jest.spyOn(sortServiceStub, 'sortHierarchicalDataset').mockReturnValue({ flat: mockFlatDataset, hierarchical: mockHierarchical });

service.init(gridStub);
const result = service.convertToHierarchicalDatasetAndSort(mockFlatDataset, [mockColumn]);
const result = service.convertToHierarchicalDatasetAndSort(mockFlatDataset, mockColumns, gridOptionsMock);

expect(setSortSpy).toHaveBeenCalledWith([{
columnId: 'file',
sortAsc: true,
sortCol: mockColumn
sortCol: mockColumns[0]
}]);
expect(result).toEqual({ flat: mockFlatDataset, hierarchical: mockHierarchical });
});
Expand All @@ -297,15 +321,36 @@ describe('TreeData Service', () => {
jest.spyOn(sortServiceStub, 'sortHierarchicalDataset').mockReturnValue({ flat: mockFlatDataset, hierarchical: mockHierarchical });

service.init(gridStub);
const result = service.convertToHierarchicalDatasetAndSort(mockFlatDataset, [mockColumn]);
const result = service.convertToHierarchicalDatasetAndSort(mockFlatDataset, mockColumns, gridOptionsMock);

expect(setSortSpy).toHaveBeenCalledWith([{
columnId: 'size',
sortAsc: false,
sortCol: mockColumn
sortCol: mockColumns[1]
}]);
expect(result).toEqual({ flat: mockFlatDataset, hierarchical: mockHierarchical });
});
});

describe('sortHierarchicalDataset method', () => {
it('should call sortHierarchicalDataset from the sort service', () => {
const mockColumns = [{ id: 'file', field: 'file', }, { id: 'size', field: 'size', }] as Column[];
const mockHierarchical = [{
id: 0,
file: 'documents',
files: [{ id: 2, file: 'todo.txt', size: 2.3, }, { id: 1, file: 'vacation.txt', size: 1.2, }]
}];
const mockColumnSort = { columnId: 'size', sortAsc: true, sortCol: mockColumns[1], }
jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(mockColumns);
const getInitialSpy = jest.spyOn(service, 'getInitialSort').mockReturnValue(mockColumnSort);
const sortHierarchySpy = jest.spyOn(sortServiceStub, 'sortHierarchicalDataset');

service.init(gridStub);
service.sortHierarchicalDataset(mockHierarchical);

expect(getInitialSpy).toHaveBeenCalledWith(mockColumns, gridOptionsMock);
expect(sortHierarchySpy).toHaveBeenCalledWith(mockHierarchical, [mockColumnSort]);
});
});
});
});
9 changes: 2 additions & 7 deletions packages/common/src/services/grid.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,19 +480,14 @@ export class GridService {

// insert position top/bottom, defaults to top
// when position is top we'll call insert at index 0, else call addItem which just push to the DataView array
if (insertPosition === 'bottom') {
if (insertPosition === 'bottom' || this._gridOptions?.enableTreeData) {
this._dataView.addItems(items);
} else {
this._dataView.insertItems(0, items); // insert at index 0 to the start of the dataset
}

// end the bulk transaction since we're all done
this._dataView.endUpdate();

// if we add/remove item(s) from the dataset, we need to also refresh our tree data filters
if (this._gridOptions?.enableTreeData) {
this.invalidateHierarchicalDataset();
}
}

if (this._gridOptions?.enableTreeData) {
Expand Down Expand Up @@ -882,7 +877,7 @@ export class GridService {
invalidateHierarchicalDataset() {
// if we add/remove item(s) from the dataset, we need to also refresh our tree data filters
if (this._gridOptions?.enableTreeData && this.treeDataService) {
const sortedDatasetResult = this.treeDataService.convertToHierarchicalDatasetAndSort(this._dataView.getItems(), this.sharedService.allColumns);
const sortedDatasetResult = this.treeDataService.convertToHierarchicalDatasetAndSort(this._dataView.getItems(), this.sharedService.allColumns, this._gridOptions);
this.sharedService.hierarchicalDataset = sortedDatasetResult.hierarchical;
this.filterService.refreshTreeDataFilters();
this._dataView.setItems(sortedDatasetResult.flat);
Expand Down
Loading

0 comments on commit 8b468f0

Please sign in to comment.