From 32291e232de2977102c38722d0eb37dba0b28c0d Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Tue, 28 May 2019 12:08:30 -0400 Subject: [PATCH 1/3] prepare release 2.7.0 --- package.json | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index cc2f1a9fc..ef8e10925 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-slickgrid", - "version": "2.6.0", + "version": "2.7.0", "description": "Slickgrid components made available in Angular", "keywords": [ "angular", @@ -91,21 +91,21 @@ "devDependencies": { "@angular-builders/jest": "^7.4.2", "@angular-devkit/build-angular": "~0.13.1", - "@angular/animations": "^7.2.8", + "@angular/animations": "^7.2.15", "@angular/cli": "^7.3.1", - "@angular/common": "^7.2.8", - "@angular/compiler": "7.2.8", - "@angular/compiler-cli": "7.2.8", - "@angular/core": "^7.2.8", - "@angular/forms": "^7.2.8", - "@angular/http": "^7.2.8", - "@angular/language-service": "^7.2.8", - "@angular/platform-browser": "^7.2.8", - "@angular/platform-browser-dynamic": "^7.2.8", - "@angular/router": "^7.2.8", + "@angular/common": "^7.2.15", + "@angular/compiler": "7.2.15", + "@angular/compiler-cli": "7.2.15", + "@angular/core": "^7.2.15", + "@angular/forms": "^7.2.15", + "@angular/http": "^7.2.15", + "@angular/language-service": "^7.2.15", + "@angular/platform-browser": "^7.2.15", + "@angular/platform-browser-dynamic": "^7.2.15", + "@angular/router": "^7.2.15", "@ng-select/ng-select": "^2.15.3", "@types/flatpickr": "^3.1.2", - "@types/jest": "^24.0.12", + "@types/jest": "^24.0.13", "@types/jquery": "^3.3.29", "@types/moment": "^2.13.0", "@types/node": "^10.12.15", @@ -116,11 +116,11 @@ "codelyzer": "~4.5.0", "copyfiles": "^2.1.0", "cross-env": "^5.2.0", - "custom-event-polyfill": "^1.0.6", + "custom-event-polyfill": "^1.0.7", "cypress": "^3.2.0", "del": "^3.0.0", "del-cli": "^1.1.0", - "gulp": "^4.0.0", + "gulp": "^4.0.2", "gulp-bump": "^3.1.3", "gulp-sass": "^4.0.2", "gulp-yuidoc": "^0.1.2", @@ -131,12 +131,12 @@ "mochawesome": "^3.1.2", "mochawesome-merge": "^1.0.7", "mochawesome-report-generator": "^3.1.5", - "ng-packagr": "^4.7.0", - "node-sass": "^4.11.0", + "ng-packagr": "^5.2.0", + "node-sass": "^4.12.0", "npm-run-all": "^4.1.5", "postcss-cli": "^6.0.1", "require-dir": "^1.2.0", - "rimraf": "^2.6.2", + "rimraf": "^2.6.3", "run-sequence": "^2.2.1", "ts-node": "~3.3.0", "tsickle": "^0.34.0", @@ -146,4 +146,4 @@ "yargs": "^12.0.5", "zone.js": "^0.8.29" } -} \ No newline at end of file +} From 5b8b4046bba74d453a0dd5510f9d83ef99c18588 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Tue, 28 May 2019 12:10:52 -0400 Subject: [PATCH 2/3] feat(gridService): add "upsertItem" method to Grid Service - also rename all CRUD methods to more simple names, however we keep previous method name with a @deprecated comment. --- src/app/examples/grid-additem.component.ts | 6 +- .../services/grid.service.ts | 167 +++++++++++++++--- 2 files changed, 141 insertions(+), 32 deletions(-) diff --git a/src/app/examples/grid-additem.component.ts b/src/app/examples/grid-additem.component.ts index 13c7b064f..e5197ff4c 100644 --- a/src/app/examples/grid-additem.component.ts +++ b/src/app/examples/grid-additem.component.ts @@ -225,10 +225,10 @@ export class GridAddItemComponent implements OnInit { updateSecondItem() { const updatedItem = this.angularGrid.gridService.getDataItemByRowNumber(1); updatedItem.duration = Math.round(Math.random() * 100); - this.angularGrid.gridService.updateDataGridItem(updatedItem); + this.angularGrid.gridService.updateItem(updatedItem); // OR by id - // this.angularGrid.gridService.updateDataGridItemById(updatedItem.id, updatedItem); + // this.angularGrid.gridService.updateItemById(updatedItem.id, updatedItem); // OR multiple changes /* @@ -236,7 +236,7 @@ export class GridAddItemComponent implements OnInit { const updatedItem2 = this.angularGrid.gridService.getDataItemByRowNumber(2); updatedItem1.duration = Math.round(Math.random() * 100); updatedItem2.duration = Math.round(Math.random() * 100); - this.angularGrid.gridService.updateDataGridItems([updatedItem1, updatedItem2], true); + this.angularGrid.gridService.updateItems([updatedItem1, updatedItem2], true); */ } } diff --git a/src/app/modules/angular-slickgrid/services/grid.service.ts b/src/app/modules/angular-slickgrid/services/grid.service.ts index 41aaf95be..2784bf3de 100644 --- a/src/app/modules/angular-slickgrid/services/grid.service.ts +++ b/src/app/modules/angular-slickgrid/services/grid.service.ts @@ -235,14 +235,25 @@ export class GridService { } } + /** @deprecated please use "addItem" method instead */ + addItemToDatagrid(item: any, shouldHighlightRow = true, shouldResortGrid = false, shouldTriggerEvent = true): number { + return this.addItem(item, shouldHighlightRow, shouldResortGrid, shouldTriggerEvent); + } + + /** @deprecated please use "addItems" method instead */ + addItemsToDatagrid(items: any[], shouldHighlightRow = true, shouldResortGrid = false, shouldTriggerEvent = true): number[] { + return this.addItems(items, shouldHighlightRow, shouldResortGrid, shouldTriggerEvent); + } + /** * Add an item (data item) to the datagrid, by default it will highlight (flashing) the inserted row but we can disable it too - * @param object dataItem: item object holding all properties of that row + * @param item object which must contain a unique "id" property and any other suitable properties * @param shouldHighlightRow do we want to highlight the row after adding item * @param shouldResortGrid defaults to false, do we want the item to be sorted after insert? When set to False, it will add item on first row (default) * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) + * @return rowIndex: typically index 0 */ - addItemToDatagrid(item: any, shouldHighlightRow = true, shouldResortGrid = false, shouldTriggerEvent = true) { + addItem(item: any, shouldHighlightRow = true, shouldResortGrid = false, shouldTriggerEvent = true): number { if (!this._grid || !this._gridOptions || !this._dataView) { throw new Error('We could not find SlickGrid Grid, DataView objects'); } @@ -254,8 +265,9 @@ export class GridService { } // highlight the row we just added, if highlight is defined + let rowNumber = 0; if (shouldHighlightRow && !shouldResortGrid) { - this.highlightRow(0, 1500); + this.highlightRow(rowNumber, 1500); } // do we want the item to be sorted in the grid, when set to False it will insert on first row (defaults to false) @@ -265,7 +277,7 @@ export class GridService { // if user wanted to see highlighted row // we need to do it here after resort and get each row number because it possibly changes after the sort if (shouldHighlightRow) { - const rowNumber = this._dataView.getRowById(item.id); + rowNumber = this._dataView.getRowById(item.id); this.highlightRow(rowNumber, 1500); } } @@ -274,24 +286,28 @@ export class GridService { if (shouldTriggerEvent) { this.onItemAdded.next(item); } + + return rowNumber; } /** * Add item array (data item) to the datagrid, by default it will highlight (flashing) the inserted row but we can disable it too - * @param dataItem array: item object holding all properties of that row + * @param item object arrays, which must contain unique "id" property and any other suitable properties * @param shouldHighlightRow do we want to highlight the row after adding item * @param shouldResortGrid defaults to false, do we want the item to be sorted after insert? When set to False, it will add item on first row (default) * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) */ - addItemsToDatagrid(items: any[], shouldHighlightRow = true, shouldResortGrid = false, shouldTriggerEvent = true) { + addItems(items: any[], shouldHighlightRow = true, shouldResortGrid = false, shouldTriggerEvent = true): number[] { let highlightRow = shouldHighlightRow; if (shouldResortGrid) { highlightRow = false; // don't highlight until later when shouldResortGrid is set to true } + const rowNumbers: number[] = []; + // loop through all items to add if (Array.isArray(items)) { - items.forEach((item: any) => this.addItemToDatagrid(item, highlightRow, false, false)); + items.forEach((item: any) => this.addItem(item, highlightRow, false, false)); } // do we want the item to be sorted in the grid, when set to False it will insert on first row (defaults to false) @@ -303,6 +319,7 @@ export class GridService { if (shouldHighlightRow) { items.forEach((item: any) => { const rowNumber = this._dataView.getRowById(item.id); + rowNumbers.push(rowNumber); this.highlightRow(rowNumber, 1500); }); } @@ -312,32 +329,54 @@ export class GridService { if (shouldTriggerEvent) { this.onItemAdded.next(items); } + + return rowNumbers; + } + + /** @deprecated please use "deleteItem" method instead */ + deleteDataGridItem(item: any, shouldTriggerEvent = true) { + this.deleteItem(item, shouldTriggerEvent); + } + + /** @deprecated please use "deleteItems" method instead */ + deleteDataGridItems(items: any[], shouldTriggerEvent = true) { + this.deleteItems(items, shouldTriggerEvent); + } + + /** @deprecated please use "deleteItemById" method instead */ + deleteDataGridItemById(itemId: string | number, shouldTriggerEvent = true) { + this.deleteItemById(itemId, shouldTriggerEvent); + } + + /** @deprecated please use "deleteItemByIds" method instead */ + deleteDataGridItemByIds(itemIds: number[] | string[], shouldTriggerEvent = true) { + this.deleteItemByIds(itemIds, shouldTriggerEvent); } /** * Delete an existing item from the datagrid (dataView) - * @param object item: item object holding all properties of that row + * @param item object which must contain a unique "id" property and any other suitable properties * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) */ - deleteDataGridItem(item: any, shouldTriggerEvent = true) { + deleteItem(item: any, shouldTriggerEvent = true) { if (!item || !item.hasOwnProperty('id')) { - throw new Error(`deleteDataGridItem() requires an item object which includes the "id" property`); + throw new Error(`deleteItem() requires an item object which includes the "id" property`); } const itemId = (!item || !item.hasOwnProperty('id')) ? undefined : item.id; - this.deleteDataGridItemById(itemId, shouldTriggerEvent); + this.deleteItemById(itemId, shouldTriggerEvent); } /** * Delete an array of existing items from the datagrid - * @param object item: item object holding all properties of that row + * @param item object which must contain a unique "id" property and any other suitable properties * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) */ - deleteDataGridItems(items: any[], shouldTriggerEvent = true) { + deleteItems(items: any[], shouldTriggerEvent = true) { // when it's not an array, we can call directly the single item delete if (!Array.isArray(items)) { - this.deleteDataGridItem(items); + this.deleteItem(items); } - items.forEach((item: any) => this.deleteDataGridItem(item, false)); + items.forEach((item: any) => this.deleteItem(item, false)); // do we want to trigger an event after deleting the item if (shouldTriggerEvent) { @@ -350,7 +389,7 @@ export class GridService { * @param itemId: item unique id * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) */ - deleteDataGridItemById(itemId: string | number, shouldTriggerEvent = true) { + deleteItemById(itemId: string | number, shouldTriggerEvent = true) { if (itemId === undefined) { throw new Error(`Cannot delete a row without a valid "id"`); } @@ -371,17 +410,17 @@ export class GridService { /** * Delete an array of existing items from the datagrid - * @param object item: item object holding all properties of that row + * @param itemIds array of item unique IDs * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) */ - deleteDataGridItemByIds(itemIds: number[] | string[], shouldTriggerEvent = true) { + deleteItemByIds(itemIds: number[] | string[], shouldTriggerEvent = true) { // when it's not an array, we can call directly the single item delete if (!Array.isArray(itemIds)) { - this.deleteDataGridItemById(itemIds); + this.deleteItemById(itemIds); } for (let i = 0; i < itemIds.length; i++) { if (itemIds[i] !== null) { - this.deleteDataGridItemById(itemIds[i], false); + this.deleteItemById(itemIds[i], false); } } @@ -391,38 +430,54 @@ export class GridService { } } + /** @deprecated please use "updateItem" method instead */ + updateDataGridItem(item: any, shouldHighlightRow = true, shouldTriggerEvent = true): number { + return this.updateItem(item, shouldHighlightRow, shouldTriggerEvent); + } + + /** @deprecated please use "updateItems" method instead */ + updateDataGridItems(items: any | any[], shouldHighlightRow = true, shouldTriggerEvent = true): number[] { + return this.updateItems(items, shouldHighlightRow, shouldTriggerEvent); + } + + /** @deprecated please use "updateItemById" method instead */ + updateDataGridItemById(itemId: number | string, item: any, shouldHighlightRow = true, shouldTriggerEvent = true): number { + return this.updateItemById(itemId, item, shouldHighlightRow, shouldTriggerEvent); + } + /** * Update an existing item with new properties inside the datagrid - * @param object item: item object holding all properties of that row + * @param item object which must contain a unique "id" property and any other suitable properties * @param shouldHighlightRow do we want to highlight the row after update * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) * @return grid row index */ - updateDataGridItem(item: any, shouldHighlightRow = true, shouldTriggerEvent = true) { + updateItem(item: any, shouldHighlightRow = true, shouldTriggerEvent = true): number { const itemId = (!item || !item.hasOwnProperty('id')) ? undefined : item.id; if (itemId === undefined) { throw new Error(`Could not find the item in the grid or it's associated "id"`); } - return this.updateDataGridItemById(itemId, item, shouldHighlightRow, shouldTriggerEvent); + return this.updateItemById(itemId, item, shouldHighlightRow, shouldTriggerEvent); } /** * Update an array of existing items with new properties inside the datagrid - * @param object item: array of item objects + * @param item object arrays, which must contain unique "id" property and any other suitable properties * @param shouldHighlightRow do we want to highlight the row after update * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) + * @return grid row indexes */ - updateDataGridItems(items: any | any[], shouldHighlightRow = true, shouldTriggerEvent = true) { + updateItems(items: any | any[], shouldHighlightRow = true, shouldTriggerEvent = true): number[] { // when it's not an array, we can call directly the single item update if (!Array.isArray(items)) { - this.updateDataGridItem(items, shouldHighlightRow); + this.updateItem(items, shouldHighlightRow); } const gridIndexes: number[] = []; items.forEach((item: any) => { - gridIndexes.push(this.updateDataGridItem(item, false, false)); + gridIndexes.push(this.updateItem(item, false, false)); }); // only highlight at the end, all at once @@ -435,17 +490,19 @@ export class GridService { if (shouldTriggerEvent) { this.onItemUpdated.next(items); } + + return gridIndexes; } /** * Update an existing item in the datagrid by it's id and new properties * @param itemId: item unique id - * @param object item: item object holding all properties of that row + * @param item object which must contain a unique "id" property and any other suitable properties * @param shouldHighlightRow do we want to highlight the row after update * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) * @return grid row index */ - updateDataGridItemById(itemId: number | string, item: any, shouldHighlightRow = true, shouldTriggerEvent = true) { + updateItemById(itemId: number | string, item: any, shouldHighlightRow = true, shouldTriggerEvent = true): number { if (itemId === undefined) { throw new Error(`Cannot update a row without a valid "id"`); } @@ -473,5 +530,57 @@ export class GridService { return gridIdx; } + return rowNumber; + } + + /** + * Insert a row into the grid if it doesn't already exist or update if it does. + * @param item object which must contain a unique "id" property and any other suitable properties + * @param shouldHighlightRow do we want to highlight the row after update + * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) + */ + upsertItem(item: any, shouldHighlightRow = true, shouldTriggerEvent = true): number { + const itemId = (!item || !item.hasOwnProperty('id')) ? undefined : item.id; + if (itemId === undefined) { + throw new Error(`The item to be Upsert in the grid must have an associated "id" for it to be valid`); + } + + const rowNumber = this._dataView.getRowById(itemId); + + if (rowNumber === undefined) { + return this.addItem(item, shouldHighlightRow, shouldTriggerEvent); + } else { + return this.updateItem(item, shouldHighlightRow, shouldTriggerEvent); + } + } + + /** + * Update an array of existing items with new properties inside the datagrid + * @param item object arrays, which must contain unique "id" property and any other suitable properties + * @param shouldHighlightRow do we want to highlight the row after update + * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) + */ + upsertItems(items: any | any[], shouldHighlightRow = true, shouldTriggerEvent = true): number[] { + // when it's not an array, we can call directly the single item update + if (!Array.isArray(items)) { + return [this.upsertItem(items, shouldHighlightRow)]; + } + + const gridIndexes: number[] = []; + items.forEach((item: any) => { + gridIndexes.push(this.upsertItem(item, false, 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 (shouldHighlightRow) { + this.highlightRow(gridIndexes); + } + + // do we want to trigger an event after updating the item + if (shouldTriggerEvent) { + this.onItemUpdated.next(items); + } + return gridIndexes; } } From eb2ec1fb2fc2c8b26f6c667e1ecbfa847f21abfd Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Wed, 29 May 2019 10:00:17 -0400 Subject: [PATCH 3/3] (feat): add UpsertItem tests --- .../services/__tests__/grid.service.spec.ts | 164 ++++++++++++++++++ .../services/grid.service.ts | 70 +++++--- 2 files changed, 214 insertions(+), 20 deletions(-) create mode 100644 src/app/modules/angular-slickgrid/services/__tests__/grid.service.spec.ts 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 new file mode 100644 index 000000000..58408f82e --- /dev/null +++ b/src/app/modules/angular-slickgrid/services/__tests__/grid.service.spec.ts @@ -0,0 +1,164 @@ +import { TestBed } from '@angular/core/testing'; +import { TranslateService, TranslateModule } from '@ngx-translate/core'; +import { GridService, ExtensionService, FilterService, GridStateService, SortService } from '..'; +import { GridOption } from '../..'; + +declare var Slick: any; +const HIGHLIGHT_TIMEOUT = 1500; + +const mockSelectionModel = jest.fn().mockImplementation(() => ({ + init: jest.fn(), + destroy: jest.fn() +})); + +jest.mock('slickgrid/plugins/slick.rowselectionmodel', () => mockSelectionModel); +Slick.RowSelectionModel = mockSelectionModel; + +let extensionServiceStub = { +} as ExtensionService; + +let filterServiceStub = { +} as FilterService; + +let gridStateServiceStub = { +} as GridStateService; + +let sortServiceStub = { +} as SortService; + +const dataviewStub = { + getIdxById: jest.fn(), + getItem: jest.fn(), + getRowById: jest.fn(), + insertItem: jest.fn(), + reSort: jest.fn(), +}; + +const gridStub = { + getOptions: jest.fn(), + getColumns: jest.fn(), + getSelectionModel: jest.fn(), + setSelectionModel: jest.fn(), + setSelectedRows: jest.fn(), + scrollRowIntoView: jest.fn(), +}; + +describe('Grid Service', () => { + let service: GridService; + let translate: TranslateService; + const gridSpy = jest.spyOn(gridStub, 'getOptions').mockReturnValue({ enableAutoResize: true } as GridOption); + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: ExtensionService, useValue: extensionServiceStub }, + { provide: FilterService, useValue: filterServiceStub }, + { provide: GridStateService, useValue: gridStateServiceStub }, + { provide: SortService, useValue: sortServiceStub }, + GridService, + ], + imports: [TranslateModule.forRoot()] + }); + translate = TestBed.get(TranslateService); + service = TestBed.get(GridService); + service.init(gridStub, dataviewStub); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create the service', () => { + expect(service).toBeTruthy(); + }); + + describe('upsertItem method', () => { + 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'); + }); + + it('should expect the service to call the "addItem" when calling "upsertItem" with the item not being found in the grid', () => { + const mockItem = { id: 0, user: { firstName: 'John', lastName: 'Doe' } }; + const dataviewSpy = jest.spyOn(dataviewStub, 'getRowById').mockReturnValue(undefined); + const serviceSpy = jest.spyOn(service, 'addItem'); + const rxOnUpsertSpy = jest.spyOn(service.onItemUpserted, 'next'); + + service.upsertItem(mockItem); + + expect(serviceSpy).toHaveBeenCalledTimes(1); + expect(dataviewSpy).toHaveBeenCalledWith(0); + expect(serviceSpy).toHaveBeenCalledWith(mockItem, true, false, true); + expect(rxOnUpsertSpy).toHaveBeenCalledWith(mockItem); + }); + + it('should expect the service to call the "addItem" when calling "upsertItems" with the items not being 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 rxOnUpsertSpy = jest.spyOn(service.onItemUpserted, 'next'); + + service.upsertItems(mockItems); + + expect(dataviewSpy).toHaveBeenCalledTimes(4); // called 4x times, 2x by the upsert itself and 2x by the addItem + expect(serviceUpsertSpy).toHaveBeenCalledTimes(2); + expect(serviceUpsertSpy).toHaveBeenNthCalledWith(1, mockItems[0], false, false, false); + expect(serviceUpsertSpy).toHaveBeenNthCalledWith(2, mockItems[1], false, false, false); + expect(serviceHighlightSpy).toHaveBeenCalledWith([0, 1]); + expect(rxOnUpsertSpy).toHaveBeenCalledWith(mockItems); + }); + + it('should expect the service to call the "upsertItem" when calling "upsertItems" with a single item which is not an array', () => { + const mockItem = { id: 0, user: { firstName: 'John', lastName: 'Doe' } }; + const dataviewSpy = jest.spyOn(dataviewStub, 'getRowById'); + const serviceUpsertSpy = jest.spyOn(service, 'upsertItem'); + const serviceHighlightSpy = jest.spyOn(service, 'highlightRow'); + const rxOnUpsertSpy = jest.spyOn(service.onItemUpserted, 'next'); + + service.upsertItems(mockItem, false, true, false); + + expect(dataviewSpy).toHaveBeenCalledTimes(2); + expect(serviceUpsertSpy).toHaveBeenCalledTimes(1); + expect(serviceUpsertSpy).toHaveBeenCalledWith(mockItem, false, true, false); + expect(serviceHighlightSpy).not.toHaveBeenCalled(); + expect(rxOnUpsertSpy).not.toHaveBeenCalled(); + }); + + it('should call the "upsertItemById" method and expect it to call the "addItem"', () => { + const mockItem = { id: 0, user: { firstName: 'John', lastName: 'Doe' } }; + expect(() => service.upsertItemById(undefined, mockItem)).toThrowError('Calling Upsert of an item requires the item to include a valid and unique "id" property'); + }); + + it('should call the "upsertItemById" method and expect it to call the "addItem" with default boolean flags', () => { + const mockItem = { id: 0, user: { firstName: 'John', lastName: 'Doe' } }; + const dataviewSpy = jest.spyOn(dataviewStub, 'getRowById').mockReturnValue(undefined); + const serviceAddItemSpy = jest.spyOn(service, 'addItem'); + const serviceHighlightSpy = jest.spyOn(service, 'highlightRow'); + const rxOnUpsertSpy = jest.spyOn(service.onItemUpserted, 'next'); + + service.upsertItemById(0, mockItem); + + expect(dataviewSpy).toHaveBeenCalledWith(0); + expect(serviceAddItemSpy).toHaveBeenCalled(); + expect(serviceAddItemSpy).toHaveBeenCalledWith(mockItem, true, false, true); + expect(serviceHighlightSpy).toHaveBeenCalledWith(0, HIGHLIGHT_TIMEOUT); + expect(rxOnUpsertSpy).toHaveBeenCalledWith(mockItem); + }); + + it('should call the "upsertItemById" method and expect it to call the "addItem" with different boolean flag provided as arguments', () => { + const mockItem = { id: 0, user: { firstName: 'John', lastName: 'Doe' } }; + const dataviewSpy = jest.spyOn(dataviewStub, 'getRowById').mockReturnValue(undefined); + const serviceAddItemSpy = jest.spyOn(service, 'addItem'); + const serviceHighlightSpy = jest.spyOn(service, 'highlightRow'); + const rxOnUpsertSpy = jest.spyOn(service.onItemUpserted, 'next'); + + service.upsertItemById(0, mockItem, false, true, false); + + expect(dataviewSpy).toHaveBeenCalledWith(0); + expect(serviceAddItemSpy).toHaveBeenCalled(); + expect(serviceAddItemSpy).toHaveBeenCalledWith(mockItem, false, true, false); + expect(serviceHighlightSpy).not.toHaveBeenCalled(); + expect(rxOnUpsertSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/modules/angular-slickgrid/services/grid.service.ts b/src/app/modules/angular-slickgrid/services/grid.service.ts index 2784bf3de..8b6b64e8a 100644 --- a/src/app/modules/angular-slickgrid/services/grid.service.ts +++ b/src/app/modules/angular-slickgrid/services/grid.service.ts @@ -1,4 +1,3 @@ -import { TranslateService } from '@ngx-translate/core'; import { Injectable } from '@angular/core'; import { CellArgs, Column, GridOption, OnEventArgs } from './../models/index'; import { ExtensionService } from './extension.service'; @@ -18,8 +17,14 @@ export class GridService { onItemAdded = new Subject(); onItemDeleted = new Subject(); onItemUpdated = new Subject(); + onItemUpserted = new Subject(); - constructor(private extensionService: ExtensionService, private filterService: FilterService, private gridStateService: GridStateService, private sortService: SortService, private translate: TranslateService) { } + constructor( + private extensionService: ExtensionService, + private filterService: FilterService, + private gridStateService: GridStateService, + private sortService: SortService + ) { } /** Getter for the Grid Options pulled through the Grid Object */ private get _gridOptions(): GridOption { @@ -360,7 +365,7 @@ export class GridService { */ deleteItem(item: any, shouldTriggerEvent = true) { if (!item || !item.hasOwnProperty('id')) { - throw new Error(`deleteItem() requires an item object which includes the "id" property`); + throw new Error(`Deleting an item requires the item to include an "id" property`); } const itemId = (!item || !item.hasOwnProperty('id')) ? undefined : item.id; this.deleteItemById(itemId, shouldTriggerEvent); @@ -374,7 +379,7 @@ export class GridService { deleteItems(items: any[], shouldTriggerEvent = true) { // when it's not an array, we can call directly the single item delete if (!Array.isArray(items)) { - this.deleteItem(items); + this.deleteItem(items, shouldTriggerEvent); } items.forEach((item: any) => this.deleteItem(item, false)); @@ -456,7 +461,7 @@ export class GridService { const itemId = (!item || !item.hasOwnProperty('id')) ? undefined : item.id; if (itemId === undefined) { - throw new Error(`Could not find the item in the grid or it's associated "id"`); + throw new Error(`Calling Update of an item requires the item to include an "id" property`); } return this.updateItemById(itemId, item, shouldHighlightRow, shouldTriggerEvent); @@ -472,7 +477,7 @@ export class GridService { updateItems(items: any | any[], shouldHighlightRow = true, shouldTriggerEvent = true): number[] { // when it's not an array, we can call directly the single item update if (!Array.isArray(items)) { - this.updateItem(items, shouldHighlightRow); + this.updateItem(items, shouldHighlightRow, shouldTriggerEvent); } const gridIndexes: number[] = []; @@ -509,7 +514,7 @@ export class GridService { const rowNumber = this._dataView.getRowById(itemId); if (!item || rowNumber === undefined) { - throw new Error(`Could not find the item in the grid or it's associated "id"`); + throw new Error(`Deleting an item requires the item to include an "id" property`); } const gridIdx = this._dataView.getIdxById(itemId); @@ -537,38 +542,35 @@ export class GridService { * Insert a row into the grid if it doesn't already exist or update if it does. * @param item object which must contain a unique "id" property and any other suitable properties * @param shouldHighlightRow do we want to highlight the row after update + * @param shouldResortGrid defaults to false, do we want the item to be sorted after insert? When set to False, it will add item on first row (default) * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) */ - upsertItem(item: any, shouldHighlightRow = true, shouldTriggerEvent = true): number { + upsertItem(item: any, shouldHighlightRow = true, shouldResortGrid = false, shouldTriggerEvent = true): number { const itemId = (!item || !item.hasOwnProperty('id')) ? undefined : item.id; + if (itemId === undefined) { - throw new Error(`The item to be Upsert in the grid must have an associated "id" for it to be valid`); + throw new Error(`Calling Upsert of an item requires the item to include an "id" property`); } - const rowNumber = this._dataView.getRowById(itemId); - - if (rowNumber === undefined) { - return this.addItem(item, shouldHighlightRow, shouldTriggerEvent); - } else { - return this.updateItem(item, shouldHighlightRow, shouldTriggerEvent); - } + return this.upsertItemById(itemId, item, shouldHighlightRow, shouldResortGrid, shouldTriggerEvent); } /** * Update an array of existing items with new properties inside the datagrid * @param item object arrays, which must contain unique "id" property and any other suitable properties * @param shouldHighlightRow do we want to highlight the row after update + * @param shouldResortGrid defaults to false, do we want the item to be sorted after insert? When set to False, it will add item on first row (default) * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) */ - upsertItems(items: any | any[], shouldHighlightRow = true, shouldTriggerEvent = true): number[] { + upsertItems(items: any | any[], shouldHighlightRow = true, shouldResortGrid = false, shouldTriggerEvent = true): number[] { // when it's not an array, we can call directly the single item update if (!Array.isArray(items)) { - return [this.upsertItem(items, shouldHighlightRow)]; + return [this.upsertItem(items, shouldHighlightRow, shouldResortGrid, shouldTriggerEvent)]; } const gridIndexes: number[] = []; items.forEach((item: any) => { - gridIndexes.push(this.upsertItem(item, false, false)); + gridIndexes.push(this.upsertItem(item, false, false, false)); }); // only highlight at the end, all at once @@ -579,8 +581,36 @@ export class GridService { // do we want to trigger an event after updating the item if (shouldTriggerEvent) { - this.onItemUpdated.next(items); + this.onItemUpserted.next(items); } return gridIndexes; } + + /** + * Update an existing item in the datagrid by it's id and new properties + * @param itemId: item unique id + * @param item object which must contain a unique "id" property and any other suitable properties + * @param shouldHighlightRow do we want to highlight the row after update + * @param shouldResortGrid defaults to false, do we want the item to be sorted after insert? When set to False, it will add item on first row (default) + * @param shouldTriggerEvent defaults to true, which will trigger an event (used by at least the pagination component) + * @return grid row index + */ + upsertItemById(itemId: number | string, item: any, shouldHighlightRow = true, shouldResortGrid = false, shouldTriggerEvent = true): number { + 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; + if (this._dataView.getRowById(itemId) === undefined) { + rowNumber = this.addItem(item, shouldHighlightRow, shouldResortGrid, shouldTriggerEvent); + } else { + rowNumber = this.updateItem(item, shouldHighlightRow, shouldTriggerEvent); + } + + // do we want to trigger an event after updating the item + if (shouldTriggerEvent) { + this.onItemUpserted.next(item); + } + return rowNumber; + } }