From 5e67331d6911fd8acd1412b42b10ce2fcd9cb01d Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 17 Aug 2026 00:12:39 -0400 Subject: [PATCH] fix(common): keep Tree Data synchronized after deletions --- .../services/__tests__/grid.service.spec.ts | 32 +++++++++++++++++++ .../__tests__/pagination.service.spec.ts | 10 ++++++ packages/common/src/services/grid.service.ts | 19 +++++++++-- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/common/src/services/__tests__/grid.service.spec.ts b/packages/common/src/services/__tests__/grid.service.spec.ts index 716dc577d4..1635ff49d5 100644 --- a/packages/common/src/services/__tests__/grid.service.spec.ts +++ b/packages/common/src/services/__tests__/grid.service.spec.ts @@ -1364,6 +1364,16 @@ describe('Grid Service', () => { expect(pubSubSpy).toHaveBeenLastCalledWith('onItemsDeleted', [mockItem.id]); }); + it('should invalidate the hierarchical dataset after deleting a Tree Data item', () => { + mockGridOptions.enableTreeData = true; + const invalidateSpy = vi.spyOn(service, 'invalidateHierarchicalDataset').mockImplementation(() => undefined); + + service.deleteItemById(4); + + expect(invalidateSpy).toHaveBeenCalledTimes(1); + delete mockGridOptions.enableTreeData; + }); + it('should remove any row selection when the grid option "enableCheckboxSelector" is enabled', () => { vi.spyOn(gridStub, 'getOptions').mockReturnValue({ enableCheckboxSelector: true } as GridOption); const mockItem = { id: 4, user: { firstName: 'John', lastName: 'Doe' } }; @@ -1404,6 +1414,17 @@ describe('Grid Service', () => { expect(pubSubSpy).toHaveBeenLastCalledWith('onItemsDeleted', mockItems); }); + it('should invalidate the hierarchical dataset once after deleting multiple Tree Data items', () => { + mockGridOptions.enableTreeData = true; + vi.spyOn(gridStub, 'getOptions').mockReturnValue(mockGridOptions); + const invalidateSpy = vi.spyOn(service, 'invalidateHierarchicalDataset').mockImplementation(() => undefined); + + service.deleteItems([{ id: 0 }, { id: 5 }]); + + expect(invalidateSpy).toHaveBeenCalledTimes(1); + delete mockGridOptions.enableTreeData; + }); + it('should expect the service to call the "deleteItem" when calling "deleteItems" with a single item which is not an array', () => { const mockItem = { id: 4, user: { firstName: 'John', lastName: 'Doe' } }; const serviceDeleteSpy = vi.spyOn(service, 'deleteItem'); @@ -1493,6 +1514,17 @@ describe('Grid Service', () => { expect(pubSubSpy).toHaveBeenCalledTimes(1); }); + it('should invalidate the hierarchical dataset once after deleting multiple Tree Data item IDs', () => { + mockGridOptions.enableTreeData = true; + vi.spyOn(gridStub, 'getOptions').mockReturnValue(mockGridOptions); + const invalidateSpy = vi.spyOn(service, 'invalidateHierarchicalDataset').mockImplementation(() => undefined); + + service.deleteItemByIds([0, 5]); + + expect(invalidateSpy).toHaveBeenCalledTimes(1); + delete mockGridOptions.enableTreeData; + }); + it('should return an empty array when argument is not an array of IDs to delete', () => { // @ts-ignore:2345 const output = service.deleteItemByIds(5, { triggerEvent: true }); diff --git a/packages/common/src/services/__tests__/pagination.service.spec.ts b/packages/common/src/services/__tests__/pagination.service.spec.ts index 10078411a8..9cc5381e21 100644 --- a/packages/common/src/services/__tests__/pagination.service.spec.ts +++ b/packages/common/src/services/__tests__/pagination.service.spec.ts @@ -716,6 +716,16 @@ describe('PaginationService', () => { }); describe('resetPagination method', () => { + it('should keep page 1 when resetting before a zero-total backend response arrives', () => { + mockGridOption.pagination!.pageNumber = 2; + mockGridOption.pagination!.totalItems = 0; + service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); + + service.resetPagination(false); + + expect(service.getCurrentPageNumber()).toBe(1); + }); + it('should call "refreshPagination" with 2 arguments True when calling the method', () => { const spy = vi.spyOn(service, 'refreshPagination'); service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); diff --git a/packages/common/src/services/grid.service.ts b/packages/common/src/services/grid.service.ts index ea082635fb..595695a4b3 100644 --- a/packages/common/src/services/grid.service.ts +++ b/packages/common/src/services/grid.service.ts @@ -1,5 +1,5 @@ import type { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; -import { arrayRemoveItemByIndex, isObjectEmpty } from '@slickgrid-universal/utils'; +import { arrayRemoveItemByIndex, isDefined, isObjectEmpty } from '@slickgrid-universal/utils'; import type { SlickDataView, SlickGrid } from '../core/index.js'; import { SlickHybridSelectionModel } from '../extensions/slickHybridSelectionModel.js'; import type { @@ -49,6 +49,7 @@ const ShowColumnOptionDefaults: ShowColumnOption = { autoResizeColumns: true, tr export class GridService { readonly pluginName = 'GridService'; protected _grid!: SlickGrid; + protected _isBulkDeletingItems = false; protected _rowSelectionPlugin?: SlickHybridSelectionModel; constructor( @@ -587,6 +588,10 @@ export class GridService { // end the bulk transaction since we're all done this._dataView.endUpdate(); + if (this._gridOptions?.enableTreeData) { + this.invalidateHierarchicalDataset(); + } + // do we want to trigger an event after deleting the item if (options.triggerEvent) { this.pubSubService.publish('onItemsDeleted', items); @@ -621,6 +626,10 @@ export class GridService { // delete the item from the dataView this._dataView.deleteItem(itemId); + if (this._gridOptions?.enableTreeData && !this._isBulkDeletingItems) { + this.invalidateHierarchicalDataset(); + } + // do we want to trigger an event after deleting the item if (options.triggerEvent) { this.pubSubService.publish>('onItemsDeleted', [itemId]); @@ -641,15 +650,21 @@ export class GridService { // begin bulk transaction this._dataView.beginUpdate(true); + this._isBulkDeletingItems = true; for (let i = 0; i < itemIds.length; i++) { - if (itemIds[i] !== null) { + if (isDefined(itemIds[i])) { this.deleteItemById(itemIds[i], { triggerEvent: false }); } } + this._isBulkDeletingItems = false; // end the bulk transaction since we're all done this._dataView.endUpdate(); + if (this._gridOptions?.enableTreeData) { + this.invalidateHierarchicalDataset(); + } + // do we want to trigger an event after deleting the item if (options.triggerEvent) { this.pubSubService.publish>('onItemsDeleted', itemIds);