diff --git a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.html b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.html index 9d984edcc..c8b0369ea 100644 --- a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.html +++ b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.html @@ -1,11 +1,16 @@
-
-
+
+
- - + +
diff --git a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts index 2020591d7..aaf6e67dd 100644 --- a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts +++ b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts @@ -7,13 +7,16 @@ import 'slickgrid/slick.grid'; import 'slickgrid/slick.dataview'; // ...then everything else... -import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Injectable, Input, Output, OnDestroy, OnInit, Optional } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, Output, OnDestroy, OnInit, Optional } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; + +import { Constants } from '../constants'; import { GlobalGridOptions } from './../global-grid-options'; import { titleCase, unsubscribeAllObservables } from './../services/utilities'; import { executeBackendProcessesCallback, onBackendError } from '../services/backend-utilities'; import { AngularGridInstance, + BackendServiceApi, BackendServiceOption, Column, ExtensionName, @@ -21,6 +24,7 @@ import { GridOption, GridStateChange, GridStateType, + Locale, Pagination, } from './../models/index'; import { FilterFactory } from '../filters/filterFactory'; @@ -63,7 +67,6 @@ declare var $: any; const slickgridEventPrefix = 'sg'; -@Injectable() @Component({ selector: 'angular-slickgrid', templateUrl: './angular-slickgrid.component.html', @@ -108,12 +111,15 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn private _hideHeaderRowAfterPageLoad = false; dataView: any; grid: any; - gridPaginationOptions: GridOption; gridHeightString: string; gridWidthString: string; groupingDefinition: any = {}; groupItemMetadataProvider: any; + backendServiceApi: BackendServiceApi; + locales: Locale; + paginationOptions: Pagination; showPagination = false; + totalItems = 0; isGridInitialized = false; subscriptions: Subscription[] = []; @@ -234,6 +240,9 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn // make sure the dataset is initialized (if not it will throw an error that it cannot getLength of null) this._dataset = this._dataset || []; this.gridOptions = this.mergeGridOptions(this.gridOptions); + this.paginationOptions = this.gridOptions.pagination; + this.locales = this.gridOptions && this.gridOptions.locales || Constants.locales; + this.backendServiceApi = this.gridOptions && this.gridOptions.backendServiceApi; this.createBackendApiInternalPostProcessCallback(this.gridOptions); if (!this.customDataView) { @@ -564,10 +573,10 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn try { // the processes can be Observables (like HttpClient) or Promises if (process instanceof Promise && process.then) { - process.then((processResult: GraphqlResult | any) => executeBackendProcessesCallback(startTime, processResult, backendApi, this.gridOptions)); + process.then((processResult: GraphqlResult | any) => executeBackendProcessesCallback(startTime, processResult, backendApi, this.gridOptions.pagination.totalItems)); } else if (isObservable(process)) { process.subscribe( - (processResult: GraphqlResult | any) => executeBackendProcessesCallback(startTime, processResult, backendApi, this.gridOptions), + (processResult: GraphqlResult | any) => executeBackendProcessesCallback(startTime, processResult, backendApi, this.gridOptions.pagination.totalItems), (error: any) => onBackendError(error, backendApi) ); } @@ -665,24 +674,25 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn this.grid.render(); } - if (this.gridOptions.backendServiceApi) { + if (this.gridOptions && this.gridOptions.backendServiceApi && this.gridOptions.pagination) { // do we want to show pagination? // if we have a backendServiceApi and the enablePagination is undefined, we'll assume that we do want to see it, else get that defined value this.showPagination = ((this.gridOptions.backendServiceApi && this.gridOptions.enablePagination === undefined) ? true : this.gridOptions.enablePagination) || false; - // before merging the grid options, make sure that it has the totalItems count - // once we have that, we can merge and pass all these options to the pagination component - if (!this.gridOptions.pagination) { - this.gridOptions.pagination = (this.gridOptions.pagination) ? this.gridOptions.pagination : undefined; - } - if (this.gridOptions.pagination && totalCount !== undefined) { - this.gridOptions.pagination.totalItems = totalCount; - } if (this.gridOptions.presets && this.gridOptions.presets.pagination && this.gridOptions.pagination) { - this.gridOptions.pagination.pageSize = this.gridOptions.presets.pagination.pageSize; - this.gridOptions.pagination.pageNumber = this.gridOptions.presets.pagination.pageNumber; + this.paginationOptions.pageSize = this.gridOptions.presets.pagination.pageSize; + this.paginationOptions.pageNumber = this.gridOptions.presets.pagination.pageNumber; } - this.gridPaginationOptions = this.mergeGridOptions(this.gridOptions); + + // when we have a totalCount use it, else we'll take it from the pagination object + // only update the total items if it's different to avoid refreshing the UI + const totalRecords = totalCount !== undefined ? totalCount : this.gridOptions.pagination.totalItems; + if (totalRecords !== this.totalItems) { + this.totalItems = totalRecords; + } + } else { + // without backend service, we'll assume the total of items is the dataset size + this.totalItems = dataset.length; } // resize the grid inside a slight timeout, in case other DOM element changed prior to the resize (like a filter/pagination changed) diff --git a/src/app/modules/angular-slickgrid/components/slick-pagination.component.html b/src/app/modules/angular-slickgrid/components/slick-pagination.component.html index c3e259b69..5fabeb440 100644 --- a/src/app/modules/angular-slickgrid/components/slick-pagination.component.html +++ b/src/app/modules/angular-slickgrid/components/slick-pagination.component.html @@ -1,4 +1,4 @@ -
+
{{textItemsPerPage}}, diff --git a/src/app/modules/angular-slickgrid/components/slick-pagination.component.ts b/src/app/modules/angular-slickgrid/components/slick-pagination.component.ts index 7bcad25e7..39fa6bede 100644 --- a/src/app/modules/angular-slickgrid/components/slick-pagination.component.ts +++ b/src/app/modules/angular-slickgrid/components/slick-pagination.component.ts @@ -2,8 +2,7 @@ import { AfterViewInit, Component, EventEmitter, Injectable, Input, OnDestroy, O import { TranslateService } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; -import { Constants } from '../constants'; -import { GridOption, Locale, Pager, Pagination } from './../models/index'; +import { BackendServiceApi, Locale, Pager, Pagination } from './../models/index'; import { PaginationService } from '../services/pagination.service'; import { unsubscribeAllObservables } from '../services/utilities'; @@ -11,26 +10,31 @@ import { unsubscribeAllObservables } from '../services/utilities'; selector: 'slick-pagination', templateUrl: './slick-pagination.component.html' }) -@Injectable() export class SlickPaginationComponent implements AfterViewInit, OnDestroy { - private _gridPaginationOptions: GridOption; private _isFirstRender = true; - private _locales: Locale; private _pager: Pager; + private _totalItems: number; private subscriptions: Subscription[] = []; @Output() onPaginationChanged = new EventEmitter(); + @Input() enableTranslate: boolean; + @Input() options: Pagination; @Input() dataView: any; + @Input() locales: Locale; + @Input() backendServiceApi: BackendServiceApi; @Input() - set gridPaginationOptions(gridPaginationOptions: GridOption) { - this._gridPaginationOptions = gridPaginationOptions; - if (this._isFirstRender || !gridPaginationOptions || !gridPaginationOptions.pagination || (gridPaginationOptions.pagination.totalItems !== this.pager.totalItems)) { - this.refreshPagination(); - this._isFirstRender = false; + set totalItems(total: number) { + if (this._isFirstRender || this._totalItems === undefined) { + this._isFirstRender = true; + } + this._totalItems = total; + this._isFirstRender = false; + if (this.paginationService) { + this.paginationService.totalItems = total; } } - get gridPaginationOptions(): GridOption { - return this._gridPaginationOptions; + get totalItems(): number { + return this._totalItems; } @Input() grid: any; @@ -44,11 +48,11 @@ export class SlickPaginationComponent implements AfterViewInit, OnDestroy { constructor(private paginationService: PaginationService, @Optional() private translate: TranslateService) { // translate all the text using ngx-translate or custom locales if (translate && translate.onLangChange) { - this.subscriptions.push(this.translate.onLangChange.subscribe(() => this.translateAllUiTexts(this._locales))); + this.subscriptions.push(this.translate.onLangChange.subscribe(() => this.translateAllUiTexts(this.locales))); } // translate all the text using ngx-translate or custom locales - this.paginationService.onPaginationRefreshed.subscribe(() => this.translateAllUiTexts(this._locales)); + this.paginationService.onPaginationRefreshed.subscribe(() => this.translateAllUiTexts(this.locales)); this.paginationService.onPaginationChanged.subscribe(pager => { this._pager = pager; @@ -74,13 +78,12 @@ export class SlickPaginationComponent implements AfterViewInit, OnDestroy { } ngAfterViewInit() { - if (this._gridPaginationOptions && this._gridPaginationOptions.enableTranslate && !this.translate) { + if (this.enableTranslate && !this.translate) { throw new Error('[Angular-Slickgrid] requires "ngx-translate" to be installed and configured when the grid option "enableTranslate" is enabled.'); } - // get locales provided by user in forRoot or else use default English locales via the Constants - this._locales = this._gridPaginationOptions && this._gridPaginationOptions.locales || Constants.locales; - - this.paginationService.init(this.grid, this.dataView, this._gridPaginationOptions); + // Angular throws the infamous "ExpressionChangedAfterItHasBeenCheckedError" + // none of the code refactoring worked to go over the error expect adding a delay, so we'll keep that for now + setTimeout(() => this.paginationService.init(this.grid, this.dataView, this.options, this.backendServiceApi)); } changeToFirstPage(event: any) { @@ -123,13 +126,6 @@ export class SlickPaginationComponent implements AfterViewInit, OnDestroy { this.subscriptions = unsubscribeAllObservables(this.subscriptions); } - refreshPagination() { - if (this.paginationService) { - this.paginationService.gridPaginationOptions = this._gridPaginationOptions; - this.paginationService.refreshPagination(); - } - } - // -- // private functions // -------------------- diff --git a/src/app/modules/angular-slickgrid/services/__tests__/backend-utilities.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/backend-utilities.spec.ts index 7597c0339..ee626b0dc 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/backend-utilities.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/backend-utilities.spec.ts @@ -16,7 +16,7 @@ describe('backend-utilities', () => { const now = new Date(); gridOptionMock.backendServiceApi.internalPostProcess = jest.fn(); const spy = jest.spyOn(gridOptionMock.backendServiceApi, 'internalPostProcess'); - executeBackendProcessesCallback(now, { data: {} }, gridOptionMock.backendServiceApi, gridOptionMock); + executeBackendProcessesCallback(now, { data: {} }, gridOptionMock.backendServiceApi, 0); expect(spy).toHaveBeenCalled(); }); @@ -46,7 +46,7 @@ describe('backend-utilities', () => { gridOptionMock.pagination = { totalItems: 1, pageSizes: [10, 25], pageSize: 10 }; const spy = jest.spyOn(gridOptionMock.backendServiceApi, 'postProcess'); - executeBackendProcessesCallback(now, mockResult, gridOptionMock.backendServiceApi, gridOptionMock); + executeBackendProcessesCallback(now, mockResult, gridOptionMock.backendServiceApi, 1); expect(spy).toHaveBeenCalledWith(expectaction); }); diff --git a/src/app/modules/angular-slickgrid/services/__tests__/grid.service.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/grid.service.spec.ts index 5a71b4486..13b57d223 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/grid.service.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/grid.service.spec.ts @@ -89,7 +89,9 @@ describe('Grid Service', () => { }); describe('upsertItem methods', () => { - jest.clearAllMocks(); + afterEach(() => { + jest.clearAllMocks(); + }); it('should throw an error when 1st argument for the item object is missing', () => { expect(() => service.upsertItem(null)).toThrowError('Calling Upsert of an item requires the item to include an "id" property'); @@ -101,8 +103,9 @@ describe('Grid Service', () => { const serviceSpy = jest.spyOn(service, 'addItem'); const rxSpy = jest.spyOn(service.onItemUpserted, 'next'); - service.upsertItem(mockItem); + const upsertRow = service.upsertItem(mockItem); + expect(upsertRow).toEqual({ added: 0, updated: undefined }); expect(serviceSpy).toHaveBeenCalledTimes(1); expect(dataviewSpy).toHaveBeenCalledWith(0); expect(serviceSpy).toHaveBeenCalledWith(mockItem, { highlightRow: true, position: 'top', resortGrid: false, selectRow: false, triggerEvent: true }); @@ -117,45 +120,81 @@ describe('Grid Service', () => { const scrollSpy = jest.spyOn(gridStub, 'scrollRowIntoView'); const rxSpy = jest.spyOn(service.onItemAdded, 'next'); - service.upsertItem(mockItem, { position: 'bottom' }); + const upsertRow = service.upsertItem(mockItem, { position: 'bottom' }); + expect(upsertRow).toEqual({ added: 1000, updated: undefined }); expect(addSpy).toHaveBeenCalledTimes(1); expect(addSpy).toHaveBeenCalledWith(mockItem); expect(scrollSpy).toHaveBeenCalledWith(expectationNewRowPosition); expect(rxSpy).toHaveBeenCalledWith(mockItem); }); - it('should expect the service to call the "updateItem" multiple times when calling "upsertItems" with the items not being found in the grid', () => { + it('should expect the service to call the "updateItem" multiple times when calling "upsertItems" with the items found in the grid', () => { const mockItems = [{ id: 0, user: { firstName: 'John', lastName: 'Doe' } }, { id: 5, user: { firstName: 'Jane', lastName: 'Doe' } }]; const dataviewSpy = jest.spyOn(dataviewStub, 'getRowById').mockReturnValue(0).mockReturnValueOnce(0).mockReturnValueOnce(0).mockReturnValueOnce(1).mockReturnValueOnce(1); const serviceUpsertSpy = jest.spyOn(service, 'upsertItem'); const serviceHighlightSpy = jest.spyOn(service, 'highlightRow'); - const rxSpy = jest.spyOn(service.onItemUpserted, 'next'); + const rxUpsertSpy = jest.spyOn(service.onItemUpserted, 'next'); + const rxAddedSpy = jest.spyOn(service.onItemAdded, 'next'); + const rxUpdatedSpy = jest.spyOn(service.onItemUpdated, 'next'); - service.upsertItems(mockItems); + const upsertRows = service.upsertItems(mockItems); - expect(dataviewSpy).toHaveBeenCalledTimes(4); // called 4x times, 2x by the upsert itself and 2x by the addItem + expect(upsertRows).toEqual([{ added: undefined, updated: 0 }, { added: undefined, updated: 1 }]); + expect(dataviewSpy).toHaveBeenCalledTimes(4); // called 4x times, 2x by the upsert itself and 2x by the updateItem expect(serviceUpsertSpy).toHaveBeenCalledTimes(2); expect(serviceUpsertSpy).toHaveBeenNthCalledWith(1, mockItems[0], { highlightRow: false, position: 'top', resortGrid: false, selectRow: false, triggerEvent: false }); expect(serviceUpsertSpy).toHaveBeenNthCalledWith(2, mockItems[1], { highlightRow: false, position: 'top', resortGrid: false, selectRow: false, triggerEvent: false }); expect(serviceHighlightSpy).toHaveBeenCalledWith([0, 1]); - expect(rxSpy).toHaveBeenCalledWith(mockItems); + expect(rxUpsertSpy).toHaveBeenCalledWith(mockItems); + expect(rxAddedSpy).toHaveBeenCalledTimes(0); + expect(rxUpdatedSpy).toHaveBeenCalledTimes(1); + expect(rxUpdatedSpy).toHaveBeenCalledWith([{ added: undefined, updated: 0 }, { added: undefined, updated: 1 }]); + }); + + it('should expect the service to call both "addItem" and "updateItem" when calling "upsertItems" with first item found but second not found', () => { + const mockItems = [{ id: 0, user: { firstName: 'John', lastName: 'Doe' } }, { id: 5, user: { firstName: 'Jane', lastName: 'Doe' } }]; + const dataviewSpy = jest.spyOn(dataviewStub, 'getRowById').mockReturnValue(undefined).mockReturnValueOnce(undefined).mockReturnValueOnce(15).mockReturnValueOnce(15); + const serviceUpsertSpy = jest.spyOn(service, 'upsertItem'); + const serviceHighlightSpy = jest.spyOn(service, 'highlightRow'); + const rxUpsertSpy = jest.spyOn(service.onItemUpserted, 'next'); + const rxAddedSpy = jest.spyOn(service.onItemAdded, 'next'); + const rxUpdatedSpy = jest.spyOn(service.onItemUpdated, 'next'); + + const upsertRows = service.upsertItems(mockItems); + + expect(upsertRows).toEqual([{ added: 0, updated: undefined }, { added: undefined, updated: 15 }]); + expect(dataviewSpy).toHaveBeenCalledTimes(3); // called 4x times, 2x by the upsert itself and 2x by the updateItem + expect(serviceUpsertSpy).toHaveBeenCalledTimes(2); + expect(serviceUpsertSpy).toHaveBeenNthCalledWith(1, mockItems[0], { highlightRow: false, position: 'top', resortGrid: false, selectRow: false, triggerEvent: false }); + expect(serviceUpsertSpy).toHaveBeenNthCalledWith(2, mockItems[1], { highlightRow: false, position: 'top', resortGrid: false, selectRow: false, triggerEvent: false }); + expect(serviceHighlightSpy).toHaveBeenCalledWith([0, 15]); + expect(rxUpsertSpy).toHaveBeenCalledWith(mockItems); + expect(rxAddedSpy).toHaveBeenCalledTimes(1); + expect(rxUpdatedSpy).toHaveBeenCalledTimes(1); + expect(rxAddedSpy).toHaveBeenCalledWith([{ added: 0, updated: undefined }]); + expect(rxUpdatedSpy).toHaveBeenCalledWith([{ added: undefined, updated: 15 }]); }); - it('should expect the service to call the "upsertItem" when calling "upsertItems" with a single item which is not an array', () => { + it('should expect the service to call the "upsertItem" when calling "upsertItems" with a single item object and without triggering an event', () => { const mockItem = { id: 0, user: { firstName: 'John', lastName: 'Doe' } }; - const dataviewSpy = jest.spyOn(dataviewStub, 'getRowById'); + const dataviewSpy = jest.spyOn(dataviewStub, 'getRowById').mockReturnValue(0).mockReturnValueOnce(0).mockReturnValueOnce(0).mockReturnValueOnce(1).mockReturnValueOnce(1);; const serviceUpsertSpy = jest.spyOn(service, 'upsertItem'); const serviceHighlightSpy = jest.spyOn(service, 'highlightRow'); - const rxSpy = jest.spyOn(service.onItemUpserted, 'next'); + const rxUpsertSpy = jest.spyOn(service.onItemUpserted, 'next'); + const rxAddedSpy = jest.spyOn(service.onItemAdded, 'next'); + const rxUpdatedSpy = jest.spyOn(service.onItemUpdated, 'next'); - service.upsertItems(mockItem, { highlightRow: false, resortGrid: true, selectRow: false, triggerEvent: false }); + const upsertRows = service.upsertItems(mockItem, { highlightRow: false, resortGrid: true, selectRow: false, triggerEvent: false }); + expect(upsertRows).toEqual([{ added: undefined, updated: 0 }]); expect(dataviewSpy).toHaveBeenCalledTimes(2); expect(serviceUpsertSpy).toHaveBeenCalledTimes(1); expect(serviceUpsertSpy).toHaveBeenCalledWith(mockItem, { highlightRow: false, position: 'top', resortGrid: true, selectRow: false, triggerEvent: false }); expect(serviceHighlightSpy).not.toHaveBeenCalled(); - expect(rxSpy).not.toHaveBeenCalled(); + expect(rxUpsertSpy).not.toHaveBeenCalled(); + expect(rxAddedSpy).toHaveBeenCalledTimes(0); + expect(rxUpdatedSpy).toHaveBeenCalledTimes(0); }); it('should expect the row to be selected when calling "upsertItems" with an item when setting the "selecRow" flag and the grid option "enableRowSelection" is set', () => { diff --git a/src/app/modules/angular-slickgrid/services/__tests__/pagination.service.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/pagination.service.spec.ts index 82f5a802f..e2b95c7d5 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/pagination.service.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/pagination.service.spec.ts @@ -89,9 +89,9 @@ describe('PaginationService', () => { it('should initialize the service and call "refreshPagination" and trigger "onPaginationChanged" event', () => { const refreshSpy = jest.spyOn(service, 'refreshPagination'); const paginationSpy = jest.spyOn(service.onPaginationChanged, 'next'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); - expect(service.gridPaginationOptions).toEqual(mockGridOption); + expect(service.paginationOptions).toEqual(mockGridOption.pagination); expect(service.pager).toBeTruthy(); expect(refreshSpy).toHaveBeenCalled(); expect(service.getCurrentPageNumber()).toBe(2); @@ -101,19 +101,40 @@ describe('PaginationService', () => { }); it('should initialize the service and be able to change the grid options by the SETTER and expect the GETTER to have updated options', () => { - const mockGridOptionCopy = { ...mockGridOption, options: null }; - service.init(gridStub, dataviewStub, mockGridOptionCopy); - service.gridPaginationOptions = mockGridOption; + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); + service.paginationOptions = mockGridOption.pagination; - expect(service.gridPaginationOptions).toEqual(mockGridOption); + expect(service.paginationOptions).toEqual(mockGridOption.pagination); expect(service.pager).toBeTruthy(); expect(service.getCurrentPageNumber()).toBe(2); }); + it('should initialize the service and be able to change the totalItems by the SETTER and not expect the "refreshPagination" method to be called within the SETTER before initialization', () => { + const spy = jest.spyOn(service, 'refreshPagination'); + service.totalItems = 125; + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); + + expect(service.totalItems).toEqual(125); + expect(service.pager).toBeTruthy(); + expect(service.getCurrentPageNumber()).toBe(2); + expect(spy).toHaveBeenCalledTimes(1); // called 1x time inside the init() only + }); + + it('should be able to change the totalItems by the SETTER after the initialization and expect the "refreshPagination" method to be called', () => { + const spy = jest.spyOn(service, 'refreshPagination'); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); + service.totalItems = 125; + + expect(service.totalItems).toEqual(125); + expect(service.pager).toBeTruthy(); + expect(service.getCurrentPageNumber()).toBe(2); + expect(spy).toHaveBeenCalledTimes(2); // called 2x times inside the init() and SETTER + }); + describe('changeItemPerPage method', () => { it('should be on page 0 when total items is 0', () => { mockGridOption.pagination.totalItems = 0; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.changeItemPerPage(30); expect(service.getCurrentPageNumber()).toBe(0); @@ -125,7 +146,7 @@ describe('PaginationService', () => { mockGridOption.pagination.pageNumber = 2; mockGridOption.pagination.totalItems = 51; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.changeItemPerPage(50); expect(service.getCurrentPageNumber()).toBe(1); @@ -137,7 +158,7 @@ describe('PaginationService', () => { mockGridOption.pagination.pageNumber = 2; mockGridOption.pagination.totalItems = 100; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.changeItemPerPage(50); expect(service.getCurrentPageNumber()).toBe(1); @@ -148,7 +169,7 @@ describe('PaginationService', () => { describe('goToFirstPage method', () => { it('should expect current page to be 1 and "processOnPageChanged" method to be called', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToFirstPage(); expect(service.pager.from).toBe(1); @@ -162,7 +183,7 @@ describe('PaginationService', () => { it('should call "goToLastPage" method and expect current page to be last page and "processOnPageChanged" method to be called', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToLastPage(); expect(service.pager.from).toBe(76); @@ -176,7 +197,7 @@ describe('PaginationService', () => { it('should expect page to increment by 1 and "processOnPageChanged" method to be called', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToNextPage(); expect(service.pager.from).toBe(51); @@ -188,7 +209,7 @@ describe('PaginationService', () => { it('should expect page to increment by 1 and "processOnPageChanged" method to be called', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToNextPage(); expect(service.pager.from).toBe(51); @@ -201,7 +222,7 @@ describe('PaginationService', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); mockGridOption.pagination.pageNumber = 4; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToNextPage(); expect(service.pager.from).toBe(76); @@ -215,7 +236,7 @@ describe('PaginationService', () => { it('should expect page to decrement by 1 and "processOnPageChanged" method to be called', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToPreviousPage(); expect(service.pager.from).toBe(1); @@ -228,7 +249,7 @@ describe('PaginationService', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); mockGridOption.pagination.pageNumber = 1; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToPreviousPage(); expect(service.pager.from).toBe(1); @@ -243,7 +264,7 @@ describe('PaginationService', () => { it('should expect page to decrement by 1 and "processOnPageChanged" method to be called', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToPageNumber(4); expect(service.pager.from).toBe(76); @@ -255,7 +276,7 @@ describe('PaginationService', () => { it('should expect to go to page 1 when input number is below 1', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToPageNumber(0); expect(service.pager.from).toBe(1); @@ -267,7 +288,7 @@ describe('PaginationService', () => { it('should expect to go to last page (4) when input number is bigger than the last page number', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToPageNumber(10); expect(service.pager.from).toBe(76); @@ -280,7 +301,7 @@ describe('PaginationService', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); mockGridOption.pagination.pageNumber = 2; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.goToPageNumber(2); expect(service.pager.from).toBe(26); @@ -302,18 +323,11 @@ describe('PaginationService', () => { }; }); - it('should throw an error when no backendServiceApi is provided', async () => { - service.init(gridStub, dataviewStub, mockGridOption); - mockGridOption.backendServiceApi = null; - - await expect(service.processOnPageChanged(1)).rejects.toThrowError(`BackendServiceApi requires the following 2 properties "process" and "service" to be defined.`); - }); - it('should execute "preProcess" method when defined', () => { const spy = jest.fn(); mockGridOption.backendServiceApi.preProcess = spy; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.processOnPageChanged(1); expect(spy).toHaveBeenCalled(); @@ -328,7 +342,7 @@ describe('PaginationService', () => { jest.spyOn(mockGridOption.backendServiceApi, 'process').mockReturnValue(promise); try { - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); await service.processOnPageChanged(1); } catch (e) { expect(mockBackendError).toHaveBeenCalledWith(mockError, mockGridOption.backendServiceApi); @@ -343,7 +357,7 @@ describe('PaginationService', () => { jest.spyOn(mockGridOption.backendServiceApi, 'process').mockReturnValue(throwError(mockError)); try { - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); await service.processOnPageChanged(1); } catch (e) { expect(mockBackendError).toHaveBeenCalledWith(mockError, mockGridOption.backendServiceApi); @@ -359,12 +373,12 @@ describe('PaginationService', () => { const promise = new Promise((resolve) => setTimeout(() => resolve(processResult), 1)); jest.spyOn(mockGridOption.backendServiceApi, 'process').mockReturnValue(promise); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.processOnPageChanged(1); setTimeout(() => { expect(postSpy).toHaveBeenCalled(); - expect(mockExecuteBackendProcess).toHaveBeenCalledWith(expect.toBeDate(), processResult, mockGridOption.backendServiceApi, mockGridOption); + expect(mockExecuteBackendProcess).toHaveBeenCalledWith(expect.toBeDate(), processResult, mockGridOption.backendServiceApi, 85); done(); }); }); @@ -377,12 +391,12 @@ describe('PaginationService', () => { const processResult = { users: [{ name: 'John' }], metrics: { startTime: now, endTime: now, executionTime: 0, totalItemCount: 0 } }; jest.spyOn(mockGridOption.backendServiceApi, 'process').mockReturnValue(of(processResult)); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.processOnPageChanged(1); setTimeout(() => { expect(postSpy).toHaveBeenCalled(); - expect(mockExecuteBackendProcess).toHaveBeenCalledWith(expect.toBeDate(), processResult, mockGridOption.backendServiceApi, mockGridOption); + expect(mockExecuteBackendProcess).toHaveBeenCalledWith(expect.toBeDate(), processResult, mockGridOption.backendServiceApi, 85); done(); }); }); @@ -394,7 +408,7 @@ describe('PaginationService', () => { mockGridOption.pagination.pageNumber = 2; mockGridOption.pagination.totalItems = 0; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.recalculateFromToIndexes(); expect(service.pager.from).toBe(0); @@ -406,7 +420,7 @@ describe('PaginationService', () => { mockGridOption.pagination.pageNumber = 2; mockGridOption.pagination.totalItems = 85; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.recalculateFromToIndexes(); expect(service.pager.from).toBe(26); @@ -418,7 +432,7 @@ describe('PaginationService', () => { mockGridOption.pagination.pageNumber = 4; mockGridOption.pagination.totalItems = 85; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.recalculateFromToIndexes(); expect(service.pager.from).toBe(76); @@ -441,7 +455,7 @@ describe('PaginationService', () => { it('should throw an error when no backendServiceApi is provided', (done) => { try { mockGridOption.backendServiceApi = null; - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); service.refreshPagination(); } catch (e) { expect(e.toString()).toContain(`BackendServiceApi requires the following 2 properties "process" and "service" to be defined.`); @@ -452,7 +466,7 @@ describe('PaginationService', () => { it('should call refreshPagination when "onFilterCleared" is triggered', () => { const spy = jest.spyOn(service, 'refreshPagination'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); filterServiceStub.onFilterCleared.next(true); expect(spy).toHaveBeenCalledWith(true); @@ -461,7 +475,7 @@ describe('PaginationService', () => { it('should call refreshPagination when "onFilterChanged" is triggered', () => { const spy = jest.spyOn(service, 'refreshPagination'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); filterServiceStub.onFilterChanged.next([{ columnId: 'field1', operator: '=', searchTerms: [] }]); expect(spy).toHaveBeenCalledWith(true); @@ -479,13 +493,16 @@ describe('PaginationService', () => { it('should call "processOnItemAddedOrRemoved" and expect the (To) to be incremented by 1 when "onItemAdded" is triggered with a single item', (done) => { const mockItems = { name: 'John' }; - const spy = jest.spyOn(service, 'recalculateFromToIndexes'); + const paginationSpy = jest.spyOn(service.onPaginationChanged, 'next'); + const recalculateSpy = jest.spyOn(service, 'recalculateFromToIndexes'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); gridServiceStub.onItemAdded.next(mockItems); setTimeout(() => { - expect(spy).toHaveBeenCalled(); + // called 2x times by init() then by processOnItemAddedOrRemoved() + expect(paginationSpy).toHaveBeenCalledTimes(2); + expect(recalculateSpy).toHaveBeenCalledTimes(2); expect(service.pager.from).toBe(26); expect(service.pager.to).toBe(50 + 1); done(); @@ -494,13 +511,16 @@ describe('PaginationService', () => { it('should call "processOnItemAddedOrRemoved" and expect the (To) to be incremented by 2 when "onItemAdded" is triggered with an array of 2 new items', (done) => { const mockItems = [{ name: 'John' }, { name: 'Jane' }]; - const spy = jest.spyOn(service, 'recalculateFromToIndexes'); + const paginationSpy = jest.spyOn(service.onPaginationChanged, 'next'); + const recalculateSpy = jest.spyOn(service, 'recalculateFromToIndexes'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); gridServiceStub.onItemAdded.next(mockItems); setTimeout(() => { - expect(spy).toHaveBeenCalled(); + // called 2x times by init() then by processOnItemAddedOrRemoved() + expect(paginationSpy).toHaveBeenCalledTimes(2); + expect(recalculateSpy).toHaveBeenCalledTimes(2); expect(service.pager.from).toBe(26); expect(service.pager.to).toBe(50 + mockItems.length); done(); @@ -508,10 +528,16 @@ describe('PaginationService', () => { }); it('should call "processOnItemAddedOrRemoved" and expect the (To) to remain the same when "onItemAdded" is triggered without any items', (done) => { - service.init(gridStub, dataviewStub, mockGridOption); + const paginationSpy = jest.spyOn(service.onPaginationChanged, 'next'); + const recalculateSpy = jest.spyOn(service, 'recalculateFromToIndexes'); + + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); gridServiceStub.onItemAdded.next(null); setTimeout(() => { + // called 1x time by init() only + expect(paginationSpy).toHaveBeenCalledTimes(1); + expect(recalculateSpy).toHaveBeenCalledTimes(1); expect(service.pager.from).toBe(26); expect(service.pager.to).toBe(50); done(); @@ -520,13 +546,16 @@ describe('PaginationService', () => { it('should call "processOnItemAddedOrRemoved" and expect the (To) to be decremented by 2 when "onItemDeleted" is triggered with a single item', (done) => { const mockItems = { name: 'John' }; - const spy = jest.spyOn(service, 'recalculateFromToIndexes'); + const paginationSpy = jest.spyOn(service.onPaginationChanged, 'next'); + const recalculateSpy = jest.spyOn(service, 'recalculateFromToIndexes'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); gridServiceStub.onItemDeleted.next(mockItems); setTimeout(() => { - expect(spy).toHaveBeenCalled(); + // called 2x times by init() then by processOnItemAddedOrRemoved() + expect(paginationSpy).toHaveBeenCalledTimes(2); + expect(recalculateSpy).toHaveBeenCalledTimes(2); expect(service.pager.from).toBe(26); expect(service.pager.to).toBe(50 - 1); done(); @@ -535,13 +564,16 @@ describe('PaginationService', () => { it('should call "processOnItemAddedOrRemoved" and expect the (To) to be decremented by 2 when "onItemDeleted" is triggered with an array of 2 new items', (done) => { const mockItems = [{ name: 'John' }, { name: 'Jane' }]; - const spy = jest.spyOn(service, 'recalculateFromToIndexes'); + const paginationSpy = jest.spyOn(service.onPaginationChanged, 'next'); + const recalculateSpy = jest.spyOn(service, 'recalculateFromToIndexes'); - service.init(gridStub, dataviewStub, mockGridOption); + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); gridServiceStub.onItemDeleted.next(mockItems); setTimeout(() => { - expect(spy).toHaveBeenCalled(); + // called 2x times by init() then by processOnItemAddedOrRemoved() + expect(paginationSpy).toHaveBeenCalledTimes(2); + expect(recalculateSpy).toHaveBeenCalledTimes(2); expect(service.pager.from).toBe(26); expect(service.pager.to).toBe(50 - mockItems.length); done(); @@ -549,10 +581,17 @@ describe('PaginationService', () => { }); it('should call "processOnItemAddedOrRemoved" and expect the (To) to remain the same when "onItemDeleted" is triggered without any items', (done) => { - service.init(gridStub, dataviewStub, mockGridOption); + const paginationSpy = jest.spyOn(service.onPaginationChanged, 'next'); + const recalculateSpy = jest.spyOn(service, 'recalculateFromToIndexes'); + + // service.totalItems = 85; + service.init(gridStub, dataviewStub, mockGridOption.pagination, mockGridOption.backendServiceApi); gridServiceStub.onItemDeleted.next(null); setTimeout(() => { + // called 1x time by init() only + expect(paginationSpy).toHaveBeenCalledTimes(1); + expect(recalculateSpy).toHaveBeenCalledTimes(1); expect(service.pager.from).toBe(26); expect(service.pager.to).toBe(50); done(); diff --git a/src/app/modules/angular-slickgrid/services/backend-utilities.ts b/src/app/modules/angular-slickgrid/services/backend-utilities.ts index 923b1613f..32e943c5d 100644 --- a/src/app/modules/angular-slickgrid/services/backend-utilities.ts +++ b/src/app/modules/angular-slickgrid/services/backend-utilities.ts @@ -5,31 +5,29 @@ import { isObservable } from 'rxjs'; * Execute the backend callback, which are mainly the "process" & "postProcess" methods. * Also note that "preProcess" was executed prior to this callback */ -export async function executeBackendCallback(query: string, args: any, startTime: Date, gridOptions: GridOption, emitActionChangedCallback: (type: EmitterType) => void) { - const backendApi = gridOptions && gridOptions.backendServiceApi; - - if (backendApi) { +export async function executeBackendCallback(backendServiceApi: BackendServiceApi, query: string, args: any, startTime: Date, totalItems: number, emitActionChangedCallback: (type: EmitterType) => void) { + if (backendServiceApi) { // emit an onFilterChanged event when it's not called by a clear filter if (args && !args.clearFilterTriggered) { emitActionChangedCallback(EmitterType.remote); } // the processes can be Observables (like HttpClient) or Promises - const process = backendApi.process(query); + const process = backendServiceApi.process(query); if (process instanceof Promise && process.then) { - process.then((processResult: GraphqlResult | any) => executeBackendProcessesCallback(startTime, processResult, backendApi, gridOptions)) - .catch((error: any) => onBackendError(error, backendApi)); + process.then((processResult: GraphqlResult | any) => executeBackendProcessesCallback(startTime, processResult, backendServiceApi, totalItems)) + .catch((error: any) => onBackendError(error, backendServiceApi)); } else if (isObservable(process)) { process.subscribe( - (processResult: GraphqlResult | any) => executeBackendProcessesCallback(startTime, processResult, backendApi, gridOptions), - (error: any) => onBackendError(error, backendApi) + (processResult: GraphqlResult | any) => executeBackendProcessesCallback(startTime, processResult, backendServiceApi, totalItems), + (error: any) => onBackendError(error, backendServiceApi) ); } } } /** Execute the Backend Processes Callback, that could come from an Observable or a Promise callback */ -export function executeBackendProcessesCallback(startTime: Date, processResult: GraphqlResult | any, backendApi: BackendServiceApi, gridOptions: GridOption): GraphqlResult | any { +export function executeBackendProcessesCallback(startTime: Date, processResult: GraphqlResult | any, backendApi: BackendServiceApi, totalItems: number): GraphqlResult | any { const endTime = new Date(); // define what our internal Post Process callback, only available for GraphQL Service for now @@ -45,8 +43,8 @@ export function executeBackendProcessesCallback(startTime: Date, processResult: startTime, endTime, executionTime: endTime.valueOf() - startTime.valueOf(), - itemCount: gridOptions && gridOptions.pagination && gridOptions.pagination.totalItems, - totalItemCount: gridOptions && gridOptions.pagination && gridOptions.pagination.totalItems + itemCount: totalItems, + totalItemCount: totalItems }; // @deprecated processResult.statistics = processResult.metrics; diff --git a/src/app/modules/angular-slickgrid/services/filter.service.ts b/src/app/modules/angular-slickgrid/services/filter.service.ts index cea2f8a2a..7485d6866 100644 --- a/src/app/modules/angular-slickgrid/services/filter.service.ts +++ b/src/app/modules/angular-slickgrid/services/filter.service.ts @@ -220,11 +220,13 @@ export class FilterService { // @deprecated, processOnFilterChanged in the future should be return as a query string NOT a Promise if (queryResponse instanceof Promise && queryResponse.then) { queryResponse.then((query: string) => { - executeBackendCallback(query, callbackArgs, new Date(), this._gridOptions, this.emitFilterChanged.bind(this)); + const totalItems = this._gridOptions && this._gridOptions.pagination && this._gridOptions.pagination.totalItems; + executeBackendCallback(backendApi, query, callbackArgs, new Date(), totalItems, this.emitFilterChanged.bind(this)); }); } else { const query = queryResponse as string; - executeBackendCallback(query, callbackArgs, new Date(), this._gridOptions, this.emitFilterChanged.bind(this)); + const totalItems = this._gridOptions && this._gridOptions.pagination && this._gridOptions.pagination.totalItems; + executeBackendCallback(backendApi, query, callbackArgs, new Date(), totalItems, this.emitFilterChanged.bind(this)); } } @@ -420,11 +422,13 @@ export class FilterService { clearTimeout(timer); timer = setTimeout(async () => { const query = await backendApi.service.processOnFilterChanged(event, args); - executeBackendCallback(query, args, startTime, this._gridOptions, this.emitFilterChanged.bind(this)); + const totalItems = this._gridOptions && this._gridOptions.pagination && this._gridOptions.pagination.totalItems; + executeBackendCallback(backendApi, query, args, startTime, totalItems, this.emitFilterChanged.bind(this)); }, debounceTypingDelay); } else { const query = await backendApi.service.processOnFilterChanged(event, args); - executeBackendCallback(query, args, startTime, this._gridOptions, this.emitFilterChanged.bind(this)); + const totalItems = this._gridOptions && this._gridOptions.pagination && this._gridOptions.pagination.totalItems; + executeBackendCallback(backendApi, query, args, startTime, totalItems, this.emitFilterChanged.bind(this)); } } } diff --git a/src/app/modules/angular-slickgrid/services/grid.service.ts b/src/app/modules/angular-slickgrid/services/grid.service.ts index f32f0ad0f..83e75d974 100644 --- a/src/app/modules/angular-slickgrid/services/grid.service.ts +++ b/src/app/modules/angular-slickgrid/services/grid.service.ts @@ -611,7 +611,7 @@ export class GridService { * @param item object which must contain a unique "id" property and any other suitable properties * @param options: provide the possibility to do certain actions after or during the upsert (highlightRow, resortGrid, selectRow, triggerEvent) */ - upsertItem(item: any, options?: GridServiceInsertOption): number { + upsertItem(item: any, options?: GridServiceInsertOption): { added: number, updated: number } { options = { ...GridServiceInsertOptionDefaults, ...options }; const itemId = (!item || !item.hasOwnProperty('id')) ? undefined : item.id; @@ -628,34 +628,43 @@ export class GridService { * @param options: provide the possibility to do certain actions after or during the upsert (highlightRow, resortGrid, selectRow, triggerEvent) * @return row numbers in the grid */ - upsertItems(items: any | any[], options?: GridServiceInsertOption): number[] { + upsertItems(items: any | any[], options?: GridServiceInsertOption): { added: number, updated: number }[] { options = { ...GridServiceInsertOptionDefaults, ...options }; // when it's not an array, we can call directly the single item update if (!Array.isArray(items)) { return [this.upsertItem(items, options)]; } - const gridRowNumbers: number[] = []; + const upsertedRows: { added: number, updated: number }[] = []; items.forEach((item: any) => { - gridRowNumbers.push(this.upsertItem(item, { ...options, highlightRow: false, resortGrid: false, selectRow: false, triggerEvent: false })); + upsertedRows.push(this.upsertItem(item, { ...options, highlightRow: false, resortGrid: false, selectRow: false, triggerEvent: false })); }); // only highlight at the end, all at once // we have to do this because doing highlight 1 by 1 would only re-select the last highlighted row which is wrong behavior if (options.highlightRow) { - this.highlightRow(gridRowNumbers); + const rowNumbers = upsertedRows.map((upsertRow) => upsertRow.added !== undefined ? upsertRow.added : upsertRow.updated); + this.highlightRow(rowNumbers); } // select the row in the grid if (options.selectRow && this._gridOptions && (this._gridOptions.enableCheckboxSelector || this._gridOptions.enableRowSelection)) { - this._grid.setSelectedRows(gridRowNumbers); + this._grid.setSelectedRows(upsertedRows); } // do we want to trigger an event after updating the item if (options.triggerEvent) { this.onItemUpserted.next(items); + const addedItems = upsertedRows.filter((upsertRow) => upsertRow.added !== undefined); + if (Array.isArray(addedItems) && addedItems.length > 0) { + this.onItemAdded.next(addedItems); + } + const updatedItems = upsertedRows.filter((upsertRow) => upsertRow.updated !== undefined); + if (Array.isArray(updatedItems) && updatedItems.length > 0) { + this.onItemUpdated.next(updatedItems); + } } - return gridRowNumbers; + return upsertedRows; } /** @@ -665,23 +674,28 @@ export class GridService { * @param options: provide the possibility to do certain actions after or during the upsert (highlightRow, resortGrid, selectRow, triggerEvent) * @return grid row number in the grid */ - upsertItemById(itemId: number | string, item: any, options?: GridServiceInsertOption): number { + upsertItemById(itemId: number | string, item: any, options?: GridServiceInsertOption): { added: number, updated: number } { + let isItemAdded = false; options = { ...GridServiceInsertOptionDefaults, ...options }; if (itemId === undefined) { throw new Error(`Calling Upsert of an item requires the item to include a valid and unique "id" property`); } - let rowNumber: number; + let rowNumberAdded: number; + let rowNumberUpdated: number; if (this._dataView.getRowById(itemId) === undefined) { - rowNumber = this.addItem(item, options); + rowNumberAdded = this.addItem(item, options); + isItemAdded = true; } else { - rowNumber = this.updateItem(item, { highlightRow: options.highlightRow, selectRow: options.selectRow, triggerEvent: options.triggerEvent }); + rowNumberUpdated = this.updateItem(item, { highlightRow: options.highlightRow, selectRow: options.selectRow, triggerEvent: options.triggerEvent }); + isItemAdded = false; } // do we want to trigger an event after updating the item if (options.triggerEvent) { this.onItemUpserted.next(item); + isItemAdded ? this.onItemAdded.next(item) : this.onItemUpdated.next(item); } - return rowNumber; + return { added: rowNumberAdded, updated: rowNumberUpdated }; } } diff --git a/src/app/modules/angular-slickgrid/services/pagination.service.ts b/src/app/modules/angular-slickgrid/services/pagination.service.ts index 5a650cfc3..cee69f655 100644 --- a/src/app/modules/angular-slickgrid/services/pagination.service.ts +++ b/src/app/modules/angular-slickgrid/services/pagination.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Subscription, isObservable, Subject } from 'rxjs'; -import { GridOption, GraphqlResult, Pager } from '../models'; +import { BackendServiceApi, GraphqlResult, Pager, Pagination } from '../models'; import { FilterService } from './filter.service'; import { GridService } from './grid.service'; import { executeBackendProcessesCallback, onBackendError } from './backend-utilities'; @@ -12,23 +12,34 @@ declare var Slick: any; @Injectable() export class PaginationService { - set gridPaginationOptions(gridPaginationOptions: GridOption) { - this._gridPaginationOptions = gridPaginationOptions; + set paginationOptions(paginationOptions: Pagination) { + this._paginationOptions = paginationOptions; } - get gridPaginationOptions(): GridOption { - return this._gridPaginationOptions; + get paginationOptions(): Pagination { + return this._paginationOptions; } + set totalItems(totalItems: number) { + this._totalItems = totalItems; + if (this._initialized) { + this.refreshPagination(); + } + } + get totalItems(): number { + return this._totalItems; + } + + private _initialized = false; + private _backendServiceApi: BackendServiceApi; private _dataFrom = 1; private _dataTo = 1; private _itemsPerPage: number; private _pageCount = 0; private _pageNumber = 1; private _totalItems = 0; - private _availablePageSizes = [25, 75, 100]; + private _availablePageSizes: number[]; private _eventHandler = new Slick.EventHandler(); - private _gridPaginationOptions: GridOption; - private _isFirstRender = true; + private _paginationOptions: Pagination; private _subscriptions: Subscription[] = []; onPaginationRefreshed = new Subject(); @@ -52,15 +63,16 @@ export class PaginationService { }; } - init(grid: any, dataView: any, gridPaginationOptions: GridOption) { + init(grid: any, dataView: any, paginationOptions: Pagination, backendServiceApi: BackendServiceApi) { + this._availablePageSizes = paginationOptions.pageSizes; this.dataView = dataView; this.grid = grid; - this._gridPaginationOptions = gridPaginationOptions; + this._backendServiceApi = backendServiceApi; + this._paginationOptions = paginationOptions; - if (!this._gridPaginationOptions || !this._gridPaginationOptions.pagination || (this._gridPaginationOptions.pagination.totalItems !== this._totalItems)) { - this.refreshPagination(); + if (!backendServiceApi || !backendServiceApi.service || !backendServiceApi.process) { + throw new Error(`BackendServiceApi requires the following 2 properties "process" and "service" to be defined.`); } - this._isFirstRender = false; // Subscribe to Filter Clear & Changed and go back to page 1 when that happen this._subscriptions.push(this.filterService.onFilterChanged.subscribe(() => this.refreshPagination(true))); @@ -72,9 +84,15 @@ export class PaginationService { this._subscriptions.push(this.gridService.onItemAdded.subscribe((items: any | any[]) => this.processOnItemAddedOrRemoved(items, true))); this._subscriptions.push(this.gridService.onItemDeleted.subscribe((items: any | any[]) => this.processOnItemAddedOrRemoved(items, false))); } + if (!this._paginationOptions || (this._paginationOptions.totalItems !== this._totalItems)) { + this.refreshPagination(); + } + this._initialized = true; } dispose() { + this._initialized = false; + // unsubscribe all SlickGrid events this._eventHandler.unsubscribeAll(); @@ -144,38 +162,35 @@ export class PaginationService { } refreshPagination(isPageNumberReset: boolean = false) { - const backendApi = this._gridPaginationOptions && this._gridPaginationOptions.backendServiceApi; - if (!backendApi || !backendApi.service || !backendApi.process) { - throw new Error(`BackendServiceApi requires the following 2 properties "process" and "service" to be defined.`); - } - // trigger an event to inform subscribers this.onPaginationRefreshed.next(true); - if (this._gridPaginationOptions && this._gridPaginationOptions.pagination) { - const pagination = this._gridPaginationOptions.pagination; + if (this._paginationOptions) { + const pagination = this._paginationOptions; // set the number of items per page if not already set if (!this._itemsPerPage) { - this._itemsPerPage = +((backendApi && backendApi.options && backendApi.options.paginationOptions && backendApi.options.paginationOptions.first) ? backendApi.options.paginationOptions.first : this._gridPaginationOptions.pagination.pageSize); + this._itemsPerPage = +((this._backendServiceApi && this._backendServiceApi.options && this._backendServiceApi.options.paginationOptions && this._backendServiceApi.options.paginationOptions.first) ? this._backendServiceApi.options.paginationOptions.first : this._paginationOptions.pageSize); } // if totalItems changed, we should always go back to the first page and recalculation the From-To indexes if (isPageNumberReset || this._totalItems !== pagination.totalItems) { - if (this._isFirstRender && pagination.pageNumber && pagination.pageNumber > 1) { - this._pageNumber = pagination.pageNumber || 1; - } else { + if (isPageNumberReset) { this._pageNumber = 1; + } else if (!this._initialized && pagination.pageNumber && pagination.pageNumber > 1) { + this._pageNumber = pagination.pageNumber || 1; } // when page number is set to 1 then also reset the "offset" of backend service if (this._pageNumber === 1) { - backendApi.service.resetPaginationOptions(); + this._backendServiceApi.service.resetPaginationOptions(); } } // calculate and refresh the multiple properties of the pagination UI - this._availablePageSizes = this._gridPaginationOptions.pagination.pageSizes; - this._totalItems = this._gridPaginationOptions.pagination.totalItems; + this._availablePageSizes = this._paginationOptions.pageSizes; + if (!this._totalItems && this._paginationOptions.totalItems) { + this._totalItems = this._paginationOptions.totalItems; + } this.recalculateFromToIndexes(); } this._pageCount = Math.ceil(this._totalItems / this._itemsPerPage); @@ -186,50 +201,43 @@ export class PaginationService { return new Promise((resolve, reject) => { this.recalculateFromToIndexes(); - const backendApi = this._gridPaginationOptions.backendServiceApi; - if (!backendApi || !backendApi.service || !backendApi.process) { - const error = new Error(`BackendServiceApi requires the following 2 properties "process" and "service" to be defined.`); - reject(error); - throw error; - } - if (this._dataTo > this._totalItems) { this._dataTo = this._totalItems; } else if (this._totalItems < this._itemsPerPage) { this._dataTo = this._totalItems; } - if (backendApi) { + if (this._backendServiceApi) { const itemsPerPage = +this._itemsPerPage; // keep start time & end timestamps & return it after process execution const startTime = new Date(); // run any pre-process, if defined, for example a spinner - if (backendApi.preProcess) { - backendApi.preProcess(); + if (this._backendServiceApi.preProcess) { + this._backendServiceApi.preProcess(); } - const query = backendApi.service.processOnPaginationChanged(event, { newPage: pageNumber, pageSize: itemsPerPage }); + const query = this._backendServiceApi.service.processOnPaginationChanged(event, { newPage: pageNumber, pageSize: itemsPerPage }); // the processes can be Promises or an Observables (like HttpClient) - const process = backendApi.process(query); + const process = this._backendServiceApi.process(query); if (process instanceof Promise) { process .then((processResult: GraphqlResult | any) => { - resolve(executeBackendProcessesCallback(startTime, processResult, backendApi, this._gridPaginationOptions)); + resolve(executeBackendProcessesCallback(startTime, processResult, this._backendServiceApi, this._totalItems)); }) .catch((error) => { - onBackendError(error, backendApi); + onBackendError(error, this._backendServiceApi); reject(process); }); } else if (isObservable(process)) { process.subscribe( (processResult: GraphqlResult | any) => { - resolve(executeBackendProcessesCallback(startTime, processResult, backendApi, this._gridPaginationOptions)); + resolve(executeBackendProcessesCallback(startTime, processResult, this._backendServiceApi, this._totalItems)); }, (error: any) => { - onBackendError(error, backendApi); + onBackendError(error, this._backendServiceApi); reject(process); } ); @@ -277,6 +285,7 @@ export class PaginationService { // finally refresh the "To" count and we know it might be different than the "items per page" count // but this is necessary since we don't want an actual backend refresh this._dataTo = previousDataTo + itemCountWithDirection; + this.onPaginationChanged.next(this.pager); } } } diff --git a/src/app/modules/angular-slickgrid/services/sort.service.ts b/src/app/modules/angular-slickgrid/services/sort.service.ts index c38fdd7c8..62f315d21 100644 --- a/src/app/modules/angular-slickgrid/services/sort.service.ts +++ b/src/app/modules/angular-slickgrid/services/sort.service.ts @@ -237,7 +237,8 @@ export class SortService { // query backend, except when it's called by a ClearFilters then we won't const query = backendApi.service.processOnSortChanged(event, args); - executeBackendCallback(query, args, startTime, gridOptions, this.emitSortChanged.bind(this)); + const totalItems = gridOptions && gridOptions.pagination && gridOptions.pagination.totalItems; + executeBackendCallback(backendApi, query, args, startTime, totalItems, this.emitSortChanged.bind(this)); } onLocalSortChanged(grid: any, dataView: any, sortColumns: ColumnSort[], forceReSort = false) {