diff --git a/package.json b/package.json index 9a0d8cab1..8c0ad9e13 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@slickgrid-universal/empty-warning-component": "^0.19.0", "@slickgrid-universal/event-pub-sub": "^0.19.0", "@slickgrid-universal/pagination-component": "^0.19.0", + "@slickgrid-universal/row-detail-view-plugin": "^0.19.0", "@slickgrid-universal/rxjs-observable": "^0.19.0", "@types/dompurify": "^2.3.1", "@types/jquery": "^3.5.8", @@ -174,4 +175,4 @@ "node": ">=14.15.0", "npm": ">=6.14.8" } -} +} \ No newline at end of file diff --git a/src/app/examples/grid-contextmenu.component.ts b/src/app/examples/grid-contextmenu.component.ts index 38c47d5e6..fe2303f52 100644 --- a/src/app/examples/grid-contextmenu.component.ts +++ b/src/app/examples/grid-contextmenu.component.ts @@ -13,6 +13,8 @@ import { Formatter, Formatters, GridOption, + SlickCellMenu, + SlickContextMenu, unsubscribeAllObservables, } from './../modules/angular-slickgrid'; @@ -108,12 +110,12 @@ export class GridContextMenuComponent implements OnInit, OnDestroy { this.angularGrid = angularGrid; } - get cellMenuInstance(): any { - return this.angularGrid?.extensionService?.getSlickgridAddonInstance?.(ExtensionName.cellMenu) ?? {}; + get cellMenuInstance(): SlickCellMenu { + return (this.angularGrid?.extensionService?.getExtensionInstanceByName?.(ExtensionName.cellMenu) ?? {}) as SlickCellMenu; } - get contextMenuInstance(): any { - return this.angularGrid?.extensionService?.getSlickgridAddonInstance?.(ExtensionName.contextMenu) ?? {}; + get contextMenuInstance(): SlickContextMenu { + return (this.angularGrid?.extensionService?.getExtensionInstanceByName?.(ExtensionName.contextMenu) ?? {}) as SlickContextMenu; } ngOnInit() { diff --git a/src/app/examples/grid-draggrouping.component.ts b/src/app/examples/grid-draggrouping.component.ts index 4a95e28ce..01766ddeb 100644 --- a/src/app/examples/grid-draggrouping.component.ts +++ b/src/app/examples/grid-draggrouping.component.ts @@ -188,7 +188,7 @@ export class GridDraggableGroupingComponent implements OnInit { } }, { - id: 'effortDriven', name: 'Effort Driven', field: 'effortDriven', + id: 'effortDriven', name: 'Effort-Driven', field: 'effortDriven', width: 80, minWidth: 20, maxWidth: 100, cssClass: 'cell-effort-driven', sortable: true, @@ -218,6 +218,7 @@ export class GridDraggableGroupingComponent implements OnInit { createPreHeaderPanel: true, showPreHeaderPanel: true, preHeaderPanelHeight: 40, + showCustomFooter: true, enableFiltering: true, // you could debounce/throttle the input text filter if you have lots of data // filterTypingDebounce: 250, diff --git a/src/app/examples/grid-frozen.component.ts b/src/app/examples/grid-frozen.component.ts index e85835368..73a8925a6 100644 --- a/src/app/examples/grid-frozen.component.ts +++ b/src/app/examples/grid-frozen.component.ts @@ -56,7 +56,7 @@ export class GridFrozenComponent implements OnInit, OnDestroy { highlightRow(event: Event, isMouseEnter: boolean) { const cell = this.gridObj.getCellFromEvent(event); - const rows = isMouseEnter ? [cell.row] : []; + const rows = isMouseEnter ? [cell?.row ?? 0] : []; this.gridObj.setSelectedRows(rows); // highlight current row event.preventDefault(); } diff --git a/src/app/examples/grid-graphql.component.ts b/src/app/examples/grid-graphql.component.ts index 3398c3c07..66df53d36 100644 --- a/src/app/examples/grid-graphql.component.ts +++ b/src/app/examples/grid-graphql.component.ts @@ -135,7 +135,7 @@ export class GridGraphqlComponent implements OnInit, OnDestroy { i18n: this.translate, gridMenu: { resizeOnShowHeaderRow: true, - customItems: [ + commandItems: [ { iconCssClass: 'fa fa-times text-danger', title: 'Reset Grid', diff --git a/src/app/examples/grid-headerbutton.component.html b/src/app/examples/grid-headerbutton.component.html index c3ab7bf64..aae058577 100644 --- a/src/app/examples/grid-headerbutton.component.html +++ b/src/app/examples/grid-headerbutton.component.html @@ -11,13 +11,21 @@

-
- - -
+
Grid 1
+ + + +
+
Grid 2 - with both Header Buttons & Menus
+ + + \ No newline at end of file diff --git a/src/app/examples/grid-headerbutton.component.scss b/src/app/examples/grid-headerbutton.component.scss new file mode 100644 index 000000000..192dcca7f --- /dev/null +++ b/src/app/examples/grid-headerbutton.component.scss @@ -0,0 +1,10 @@ +/* 1st grid */ +#grid7-1 { + --slick-header-button-float: right; +} + +/* 2nd grid */ +#grid7-2 { + --slick-header-button-margin: 4px 0 50px 0; + --slick-header-button-float: left; +} \ No newline at end of file diff --git a/src/app/examples/grid-headerbutton.component.ts b/src/app/examples/grid-headerbutton.component.ts index d12f8fa84..74404427c 100644 --- a/src/app/examples/grid-headerbutton.component.ts +++ b/src/app/examples/grid-headerbutton.component.ts @@ -1,18 +1,14 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; -import { Column, Formatter, GridOption, SlickDataView, SlickGrid } from './../modules/angular-slickgrid'; +import { AngularGridInstance, Column, GridOption, } from './../modules/angular-slickgrid'; // create a custom Formatter to highlight negative values in red -let columnsWithHighlightingById = {}; -const highlightingFormatter: Formatter = (_row, _cell, value, columnDef) => { - if (columnsWithHighlightingById && (columnsWithHighlightingById as any)[columnDef.id] && value < 0) { - return `
${value}
`; - } else { - return value; - } -}; +let columns1WithHighlightingById: any = {}; +let columns2WithHighlightingById: any = {}; @Component({ + styleUrls: ['./grid-headerbutton.component.scss'], + encapsulation: ViewEncapsulation.None, templateUrl: './grid-headerbutton.component.html' }) export class GridHeaderButtonComponent implements OnInit { @@ -25,7 +21,6 @@ export class GridHeaderButtonComponent implements OnInit {
  • Resize the 1st column to see all icon/command
  • Mouse hover the 2nd column to see it's icon/command
  • For all the other columns, click on top-right red circle icon to enable highlight of negative numbers.
  • -
  • Note: The "Header Button" & "Header Menu" Plugins cannot be used at the same time
  • Use override callback functions to change the properties of show/hide, enable/disable the menu or certain item(s) from the list
    1. These callbacks are: "itemVisibilityOverride", "itemUsabilityOverride"
    2. @@ -35,16 +30,38 @@ export class GridHeaderButtonComponent implements OnInit { `; - columnDefinitions!: Column[]; - gridOptions!: GridOption; - dataset!: any[]; - gridObj: any; - dataviewObj: any; - visibleColumns!: Column[]; + columnDefinitions1: Column[] = []; + columnDefinitions2: Column[] = []; + gridOptions1!: GridOption; + gridOptions2!: GridOption; + dataset1: any[] = []; + dataset2: any[] = []; + angularGrid1!: AngularGridInstance; + angularGrid2!: AngularGridInstance; + + constructor() { + columns1WithHighlightingById = {}; + columns2WithHighlightingById = {}; + } ngOnInit(): void { - this.columnDefinitions = []; - this.gridOptions = { + this.defineGrid(); + + // populate the dataset once the grid is ready + this.dataset1 = this.loadData(200, 1); + this.dataset2 = this.loadData(200, 2); + } + + angularGrid1Ready(angularGrid: AngularGridInstance) { + this.angularGrid1 = angularGrid; + } + + angularGrid2Ready(angularGrid: AngularGridInstance) { + this.angularGrid2 = angularGrid; + } + + defineGrid() { + this.gridOptions1 = { enableAutoResize: true, enableHeaderButton: true, enableHeaderMenu: false, @@ -53,62 +70,94 @@ export class GridHeaderButtonComponent implements OnInit { rightPadding: 10 }, enableFiltering: false, + enableExcelCopyBuffer: true, + excelCopyBufferOptions: { + onCopyCells: (e, args) => console.log('onCopyCells', e, args), + onPasteCells: (e, args) => console.log('onPasteCells', e, args), + onCopyCancelled: (e, args) => console.log('onCopyCancelled', e, args), + }, enableCellNavigation: true, + gridHeight: 275, headerButton: { // you can use the "onCommand" (in Grid Options) and/or the "action" callback (in Column Definition) - onCommand: (e, args) => { - const column = args.column; - const button = args.button; - const command = args.command; - if (!columnsWithHighlightingById) { - columnsWithHighlightingById = {}; - } - - if (command === 'toggle-highlight') { - if (button.cssClass === 'fa fa-circle red') { - delete (columnsWithHighlightingById as any)[column.id]; - button.cssClass = 'fa fa-circle-o red faded'; - button.tooltip = 'Highlight negative numbers.'; - } else { - (columnsWithHighlightingById as any)[column.id] = true; - button.cssClass = 'fa fa-circle red'; - button.tooltip = 'Remove highlight.'; - } + onCommand: (_e, args) => this.handleOnCommand(_e, args, 1) + } + }; - this.gridObj.invalidate(); - } - } + // grid 2 options, same as grid 1 + extras + this.gridOptions2 = { + ...this.gridOptions1, + enableHeaderMenu: true, + enableFiltering: true, + // frozenColumn: 2, + // frozenRow: 2, + headerButton: { + // when floating to left, you might want to inverse the icon orders + onCommand: (_e, args) => this.handleOnCommand(_e, args, 2) } }; + } - this.getData(); + handleOnCommand(_e: Event, args: any, gridNo: 1 | 2) { + const column = args.column; + const button = args.button; + const command = args.command; + + if (command === 'toggle-highlight') { + if (button.cssClass === 'fa fa-circle red') { + if (gridNo === 1) { + delete columns1WithHighlightingById[column.id]; + } else { + delete columns2WithHighlightingById[column.id]; + } + button.cssClass = 'fa fa-circle-o red faded'; + button.tooltip = 'Highlight negative numbers.'; + } else { + if (gridNo === 1) { + columns1WithHighlightingById[column.id] = true; + } else { + columns2WithHighlightingById[column.id] = true; + } + button.cssClass = 'fa fa-circle red'; + button.tooltip = 'Remove highlight.'; + } + ((this as any)[`angularGrid${gridNo}`] as AngularGridInstance).slickGrid.invalidate(); + } } - getData() { + loadData(count: number, gridNo: 1 | 2) { // Set up some test columns. for (let i = 0; i < 10; i++) { - this.columnDefinitions.push({ + (this as any)[`columnDefinitions${gridNo}`].push({ id: i, name: 'Column ' + String.fromCharCode('A'.charCodeAt(0) + i), field: i + '', width: i === 0 ? 70 : 100, // have the 2 first columns wider + filterable: true, sortable: true, - formatter: highlightingFormatter, + formatter: (_row: number, _cell: number, value: any, columnDef: Column) => { + if (gridNo === 1 && columns1WithHighlightingById[columnDef.id] && value < 0) { + return `
      ${value}
      `; + } else if (gridNo === 2 && columns2WithHighlightingById[columnDef.id] && value < 0) { + return `
      ${value}
      `; + } + return value; + }, header: { buttons: [ { cssClass: 'fa fa-circle-o red faded', command: 'toggle-highlight', tooltip: 'Highlight negative numbers.', - itemVisibilityOverride: (args) => { + itemVisibilityOverride: (args: any) => { // for example don't show the header button on column "E" return args.column.name !== 'Column E'; }, - itemUsabilityOverride: (args) => { + itemUsabilityOverride: (args: any) => { // for example the button usable everywhere except on last column ='J" return args.column.name !== 'Column J'; }, - action: (e, args) => { + action: (_e: Event, args: any) => { // you can use the "action" callback and/or subscribe to the "onCallback" event, they both have the same arguments // do something console.log(`execute a callback action to "${args.command}" on ${args.column.name}`); @@ -120,45 +169,50 @@ export class GridHeaderButtonComponent implements OnInit { } // Set multiple buttons on the first column to demonstrate overflow. - this.columnDefinitions[0].name = 'Resize me!'; - this.columnDefinitions[0].header = { + (this as any)[`columnDefinitions${gridNo}`][0].name = 'Resize me!'; + (this as any)[`columnDefinitions${gridNo}`][0].header = { buttons: [ { cssClass: 'fa fa-tag', - handler: (e) => { + handler: () => { alert('Tag'); } }, { cssClass: 'fa fa-comment', - handler: (e) => { + handler: () => { alert('Comment'); } }, { cssClass: 'fa fa-info-circle', - handler: (e) => { + handler: () => { alert('Info'); } }, { cssClass: 'fa fa-question-circle', - handler: (e) => { + handler: () => { alert('Help'); } } ] }; + // when floating to left, you might want to inverse the icon orders + if (gridNo === 2) { + this.columnDefinitions2[0].header?.buttons?.reverse(); + } + // Set a button on the second column to demonstrate hover. - this.columnDefinitions[1].name = 'Hover me!'; - this.columnDefinitions[1].header = { + (this as any)[`columnDefinitions${gridNo}`][1].name = 'Hover me!'; + (this as any)[`columnDefinitions${gridNo}`][1].header = { buttons: [ { cssClass: 'fa fa-question-circle', showOnHover: true, tooltip: 'This button only appears on hover.', - handler: (e) => { + handler: () => { alert('Help'); } } @@ -167,20 +221,13 @@ export class GridHeaderButtonComponent implements OnInit { // mock a dataset const mockDataset = []; - for (let i = 0; i < 100; i++) { + for (let i = 0; i < count; i++) { const d: any = (mockDataset[i] = {}); d['id'] = i; - for (let j = 0; j < this.columnDefinitions.length; j++) { + for (let j = 0; j < (this as any)[`columnDefinitions${gridNo}`].length; j++) { d[j] = Math.round(Math.random() * 10) - 5; } } - this.dataset = mockDataset; - } - - gridReady(grid: SlickGrid) { - this.gridObj = grid; - } - dataviewReady(dataview: SlickDataView) { - this.dataviewObj = dataview; + return mockDataset; } -} +} \ No newline at end of file diff --git a/src/app/examples/grid-menu.component.ts b/src/app/examples/grid-menu.component.ts index 5643c6cf5..51f297b1e 100644 --- a/src/app/examples/grid-menu.component.ts +++ b/src/app/examples/grid-menu.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; -import { AngularGridInstance, Column, ExtensionName, FieldType, Filters, Formatters, GridOption, unsubscribeAllObservables } from './../modules/angular-slickgrid'; +import { AngularGridInstance, Column, ExtensionName, FieldType, Filters, Formatters, GridOption, SlickGridMenu, unsubscribeAllObservables } from './../modules/angular-slickgrid'; import { TranslateService } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; @@ -88,12 +88,6 @@ export class GridMenuComponent implements OnInit, OnDestroy { // we could disable the menu entirely by returning false depending on some code logic menuUsabilityOverride: (args) => true, - // use the click event position to reposition the grid menu (defaults to false) - // basically which offset do we want to use for reposition the grid menu, - // option1 is where we clicked (true) or option2 is where the icon button is located (false and is the defaults) - // you probably want to set this to True if you use an external grid menu button BUT set to False when using default grid menu - useClickToRepositionMenu: true, - // all titles optionally support translation keys, if you wish to use that feature then use the title properties with the 'Key' suffix (e.g: titleKey) // example "customTitle" for a plain string OR "customTitleKey" to use a translation key customTitleKey: 'CUSTOM_COMMANDS', @@ -103,7 +97,7 @@ export class GridMenuComponent implements OnInit, OnDestroy { hideToggleFilterCommand: false, // show/hide internal custom commands menuWidth: 17, resizeOnShowHeaderRow: true, - customItems: [ + commandItems: [ // add Custom Items Commands which will be appended to the existing internal custom items // you cannot override an internal items but you can hide them and create your own // also note that the internal custom commands are in the positionOrder range of 50-60, @@ -215,10 +209,12 @@ export class GridMenuComponent implements OnInit, OnDestroy { ); } - toggleGridMenu(e: Event) { + toggleGridMenu(e: any) { if (this.angularGrid && this.angularGrid.extensionService) { - const gridMenuInstance = this.angularGrid.extensionService.getSlickgridAddonInstance(ExtensionName.gridMenu); - gridMenuInstance.showGridMenu(e); + const gridMenuInstance = this.angularGrid.extensionService.getExtensionInstanceByName(ExtensionName.gridMenu) as SlickGridMenu; + // open the external button Grid Menu, you can also optionally pass Grid Menu options as 2nd argument + // for example we want to align our external button on the right without affecting the menu within the grid which will stay aligned on the left + gridMenuInstance.showGridMenu(e, { dropSide: 'right' }); } } diff --git a/src/app/examples/grid-rowdetail.component.ts b/src/app/examples/grid-rowdetail.component.ts index 8a9e09fa9..e12845160 100644 --- a/src/app/examples/grid-rowdetail.component.ts +++ b/src/app/examples/grid-rowdetail.component.ts @@ -6,7 +6,8 @@ import { FieldType, Filters, Formatters, - GridOption + GridOption, + SlickRowDetailView } from './../modules/angular-slickgrid'; import { RowDetailViewComponent } from './rowdetail-view.component'; import { RowDetailPreloadComponent } from './rowdetail-preload.component'; @@ -42,14 +43,14 @@ export class GridRowDetailComponent implements OnInit { this.angularGrid = angularGrid; } - get rowDetailInstance(): any { + get rowDetailInstance(): SlickRowDetailView { // you can get the SlickGrid RowDetail plugin (addon) instance via 2 ways // option 1 - return this.angularGrid.extensions.rowDetailView.instance || {}; + return (this.angularGrid.extensions.rowDetailView.instance || {}) as SlickRowDetailView; // OR options 2 - // return this.angularGrid && this.angularGrid.extensionService.getSlickgridAddonInstance(ExtensionName.rowDetailView) || {}; + // return (this.angularGrid && this.angularGrid.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView) || {}) as SlickRowDetailView; } ngOnInit(): void { @@ -119,7 +120,7 @@ export class GridRowDetailComponent implements OnInit { // you can override the logic for showing (or not) the expand icon // for example, display the expand icon only on every 2nd row - // expandableOverride: (row: number, dataContext: any, grid: SlickGrid) => (dataContext.id % 2 === 1), + // expandableOverride: (row: number, dataContext: any, grid: SlickGrid) => (dataContext.rowId % 2 === 1), // Preload View Component preloadComponent: RowDetailPreloadComponent, diff --git a/src/app/examples/grid-rowmove.component.ts b/src/app/examples/grid-rowmove.component.ts index 41d1f84fc..61df71f99 100644 --- a/src/app/examples/grid-rowmove.component.ts +++ b/src/app/examples/grid-rowmove.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; -import { AngularGridInstance, Column, ExtensionName, Filters, Formatters, GridOption } from './../modules/angular-slickgrid'; +import { AngularGridInstance, Column, ExtensionName, Filters, Formatters, GridOption, SlickRowMoveManager } from './../modules/angular-slickgrid'; @Component({ templateUrl: './grid-rowmove.component.html' @@ -32,8 +32,8 @@ export class GridRowMoveComponent implements OnInit { this.angularGrid = angularGrid; } - get rowMoveInstance(): any { - return this.angularGrid?.extensionService?.getSlickgridAddonInstance?.(ExtensionName.rowMoveManager) ?? {}; + get rowMoveInstance(): SlickRowMoveManager { + return (this.angularGrid?.extensionService?.getExtensionInstanceByName?.(ExtensionName.rowMoveManager) ?? {}) as SlickRowMoveManager; } ngOnInit(): void { diff --git a/src/app/modules/angular-slickgrid/components/__tests__/angular-slickgrid.component.spec.ts b/src/app/modules/angular-slickgrid/components/__tests__/angular-slickgrid.component.spec.ts index 94dde35c6..c9792581e 100644 --- a/src/app/modules/angular-slickgrid/components/__tests__/angular-slickgrid.component.spec.ts +++ b/src/app/modules/angular-slickgrid/components/__tests__/angular-slickgrid.component.spec.ts @@ -35,17 +35,18 @@ import { SlickGrid, SlickDraggableGrouping, SortService, - TreeDataService + TreeDataService, + SlickGroupItemMetadataProvider } from '@slickgrid-universal/common'; import * as formatterUtilities from '@slickgrid-universal/common/dist/commonjs/formatters/formatterUtilities'; import { SlickFooterComponent } from '@slickgrid-universal/custom-footer-component'; import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; import { SlickEmptyWarningComponent } from '@slickgrid-universal/empty-warning-component'; import { GraphqlPaginatedResult, GraphqlService, GraphqlServiceApi, GraphqlServiceOption } from '@slickgrid-universal/graphql'; -import { TextExportService } from '@slickgrid-universal/text-export'; import { of, throwError } from 'rxjs'; import { AngularSlickgridComponent } from '../angular-slickgrid.component'; +import { SlickRowDetailView } from '../../extensions/slickRowDetailView'; import { TranslaterServiceStub } from '../../../../../../test/translaterServiceStub'; import { AngularUtilService, ContainerService, TranslaterService } from '../../services'; import { GridOption } from '../../models'; @@ -58,7 +59,15 @@ const mockAutoAddCustomEditorFormatter = jest.fn(); declare const Slick: any; const slickEventHandler = new MockSlickEventHandler(); jest.mock('flatpickr', () => { }); -const sharedService = new SharedService(); + +const mockSlickRowDetailView = { + create: jest.fn(), + init: jest.fn(), +} as unknown as SlickRowDetailView; + +jest.mock('../../extensions/slickRowDetailView', () => ({ + SlickRowDetailView: jest.fn().mockImplementation(() => mockSlickRowDetailView), +})); const angularUtilServiceStub = { createAngularComponent: jest.fn(), @@ -84,16 +93,13 @@ const mockAppRef = { } as unknown as ApplicationRef; const extensionServiceStub = { + addExtensionToList: jest.fn(), bindDifferentExtensions: jest.fn(), createExtensionsBeforeGridCreation: jest.fn(), dispose: jest.fn(), renderColumnHeaders: jest.fn(), - translateCellMenu: jest.fn(), + translateAllExtensions: jest.fn(), translateColumnHeaders: jest.fn(), - translateColumnPicker: jest.fn(), - translateContextMenu: jest.fn(), - translateGridMenu: jest.fn(), - translateHeaderMenu: jest.fn(), } as unknown as ExtensionService; Object.defineProperty(extensionServiceStub, 'extensionList', { get: jest.fn(() => { }), set: jest.fn(), configurable: true }); @@ -718,7 +724,6 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () = describe('use grouping', () => { it('should load groupItemMetaProvider to the DataView when using "draggableGrouping" feature', () => { const dataviewSpy = jest.spyOn(mockDataViewImplementation.prototype, 'constructor'); - const groupMetaSpy = jest.spyOn(mockGroupItemMetaProviderImplementation.prototype, 'constructor'); const sharedMetaSpy = jest.spyOn(SharedService.prototype, 'groupItemMetadataProvider', 'set'); jest.spyOn(extensionServiceStub, 'extensionList', 'get').mockReturnValue({ draggableGrouping: { pluginName: 'DraggableGrouping' } } as unknown as ExtensionList); @@ -726,8 +731,8 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () = component.initialization(slickEventHandler); expect(dataviewSpy).toHaveBeenCalledWith({ inlineFilters: false, groupItemMetadataProvider: expect.anything() }); - expect(groupMetaSpy).toHaveBeenCalledWith(); - expect(sharedMetaSpy).toHaveBeenCalledWith(mockGroupItemMetaProvider); + expect(sharedService.groupItemMetadataProvider instanceof SlickGroupItemMetadataProvider).toBeTruthy(); + expect(sharedMetaSpy).toHaveBeenCalledWith(expect.toBeObject()); component.destroy(); }); @@ -741,8 +746,8 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () = component.initialization(slickEventHandler); expect(dataviewSpy).toHaveBeenCalledWith({ inlineFilters: false, groupItemMetadataProvider: expect.anything() }); - expect(groupMetaSpy).toHaveBeenCalledWith(); - expect(sharedMetaSpy).toHaveBeenCalledWith(mockGroupItemMetaProvider); + expect(sharedMetaSpy).toHaveBeenCalledWith(expect.toBeObject()); + expect(sharedService.groupItemMetadataProvider instanceof SlickGroupItemMetadataProvider).toBeTruthy(); component.destroy(); }); @@ -897,24 +902,25 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () = expect(spy).not.toHaveBeenCalled(); }); - it('should call "translateColumnHeaders" from ExtensionService when "enableTranslate" is set', () => { - const spy = jest.spyOn(extensionServiceStub, 'translateColumnHeaders'); + it('should create the Row Detail View plugin when "enableRowDetailView" is enabled', () => { + const initSpy = jest.spyOn(mockSlickRowDetailView, 'init'); + const createSpy = jest.spyOn(mockSlickRowDetailView, 'create'); - component.gridOptions = { enableTranslate: true } as unknown as GridOption; + component.gridOptions = { enableRowDetailView: true } as unknown as GridOption; component.initialization(slickEventHandler); - expect(spy).toHaveBeenCalled(); + expect(component.registeredResources.length).toBe(4); + expect(createSpy).toHaveBeenCalled(); + expect(initSpy).toHaveBeenCalled(); }); - it('should initialize ExportService when "enableTextExport" is set when using Salesforce', () => { - const fileExportMock = new TextExportService(); - const fileExportSpy = jest.spyOn(fileExportMock, 'init'); - component.gridOptions = { enableTextExport: true, registerExternalResources: [fileExportMock] } as GridOption; + it('should call "translateColumnHeaders" from ExtensionService when "enableTranslate" is set', () => { + const spy = jest.spyOn(extensionServiceStub, 'translateColumnHeaders'); + + component.gridOptions = { enableTranslate: true } as unknown as GridOption; component.initialization(slickEventHandler); - expect(fileExportSpy).toHaveBeenCalled(); - expect(component.registeredResources.length).toBe(4); // TextExportService, GridService, GridStateService, SlickEmptyCompositeEditorComponent - expect(component.registeredResources[0] instanceof TextExportService).toBeTrue(); + expect(spy).toHaveBeenCalled(); }); it('should add RxJS resource to all necessary Services when RxJS external resource is registered', () => { @@ -1302,12 +1308,7 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () = }); it('should call multiple translate methods when locale changes', (done) => { - const transCellMenuSpy = jest.spyOn(extensionServiceStub, 'translateCellMenu'); - const transColHeaderSpy = jest.spyOn(extensionServiceStub, 'translateColumnHeaders'); - const transColPickerSpy = jest.spyOn(extensionServiceStub, 'translateColumnPicker'); - const transContextMenuSpy = jest.spyOn(extensionServiceStub, 'translateContextMenu'); - const transGridMenuSpy = jest.spyOn(extensionServiceStub, 'translateGridMenu'); - const transHeaderMenuSpy = jest.spyOn(extensionServiceStub, 'translateHeaderMenu'); + const transAllExtSpy = jest.spyOn(extensionServiceStub, 'translateAllExtensions'); const transGroupingColSpanSpy = jest.spyOn(groupingAndColspanServiceStub, 'translateGroupingAndColSpan'); const setHeaderRowSpy = jest.spyOn(mockGrid, 'setHeaderRowVisibility'); @@ -1319,24 +1320,14 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () = setTimeout(() => { expect(setHeaderRowSpy).not.toHaveBeenCalled(); expect(transGroupingColSpanSpy).not.toHaveBeenCalled(); - expect(transCellMenuSpy).toHaveBeenCalled(); - expect(transColHeaderSpy).toHaveBeenCalled(); - expect(transColPickerSpy).toHaveBeenCalled(); - expect(transContextMenuSpy).toHaveBeenCalled(); - expect(transGridMenuSpy).toHaveBeenCalled(); - expect(transHeaderMenuSpy).toHaveBeenCalled(); + expect(transAllExtSpy).toHaveBeenCalled(); done(); }); }); it('should call "setHeaderRowVisibility", "translateGroupingAndColSpan" and other methods when locale changes', (done) => { component.columnDefinitions = [{ id: 'firstName', field: 'firstName', filterable: true }]; - const transCellMenuSpy = jest.spyOn(extensionServiceStub, 'translateCellMenu'); - const transColHeaderSpy = jest.spyOn(extensionServiceStub, 'translateColumnHeaders'); - const transColPickerSpy = jest.spyOn(extensionServiceStub, 'translateColumnPicker'); - const transContextMenuSpy = jest.spyOn(extensionServiceStub, 'translateContextMenu'); - const transGridMenuSpy = jest.spyOn(extensionServiceStub, 'translateGridMenu'); - const transHeaderMenuSpy = jest.spyOn(extensionServiceStub, 'translateHeaderMenu'); + const transAllExtSpy = jest.spyOn(extensionServiceStub, 'translateAllExtensions'); const transGroupingColSpanSpy = jest.spyOn(groupingAndColspanServiceStub, 'translateGroupingAndColSpan'); component.gridOptions = { enableTranslate: true, createPreHeaderPanel: true, enableDraggableGrouping: false } as unknown as GridOption; @@ -1346,12 +1337,7 @@ describe('Angular-Slickgrid Custom Component instantiated via Constructor', () = setTimeout(() => { expect(transGroupingColSpanSpy).toHaveBeenCalled(); - expect(transCellMenuSpy).toHaveBeenCalled(); - expect(transColHeaderSpy).toHaveBeenCalled(); - expect(transColPickerSpy).toHaveBeenCalled(); - expect(transContextMenuSpy).toHaveBeenCalled(); - expect(transGridMenuSpy).toHaveBeenCalled(); - expect(transHeaderMenuSpy).toHaveBeenCalled(); + expect(transAllExtSpy).toHaveBeenCalled(); done(); }); }); 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 cc526c004..8e9fbd074 100644 --- a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts +++ b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts @@ -8,7 +8,6 @@ import 'slickgrid/lib/jquery.mousewheel'; import 'slickgrid/slick.core'; import 'slickgrid/slick.grid'; import 'slickgrid/slick.dataview'; -import 'slickgrid/slick.groupitemmetadataprovider'; // ...then everything else... import { AfterViewInit, ApplicationRef, ChangeDetectorRef, Component, ElementRef, Inject, Input, OnDestroy, Optional, } from '@angular/core'; @@ -23,6 +22,7 @@ import { Column, ColumnEditor, DataViewOption, + ExtensionName, ExternalResource, Locale, Metrics, @@ -39,6 +39,7 @@ import { CollectionService, EventNamingStyle, ExtensionService, + ExtensionUtility, FilterFactory, FilterService, GridEventService, @@ -53,27 +54,11 @@ import { SortService, TreeDataService, - // extensions - AutoTooltipExtension, - CheckboxSelectorExtension, - CellExternalCopyManagerExtension, - CellMenuExtension, - ColumnPickerExtension, - ContextMenuExtension, - DraggableGroupingExtension, - ExtensionUtility, - GridMenuExtension, - GroupItemMetaProviderExtension, - HeaderMenuExtension, - HeaderButtonExtension, - RowSelectionExtension, - RowMoveManagerExtension, - // utilities autoAddEditorFormatterToColumnsWithEditor, emptyElement, - GetSlickEventType, GridStateType, + SlickGroupItemMetadataProvider, } from '@slickgrid-universal/common'; import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; import { SlickEmptyWarningComponent } from '@slickgrid-universal/empty-warning-component'; @@ -90,7 +75,7 @@ import { unsubscribeAllObservables } from './../services/utilities'; // Services import { AngularUtilService } from '../services/angularUtil.service'; -import { RowDetailViewExtension } from '../extensions/rowDetailViewExtension'; +import { SlickRowDetailView } from '../extensions/slickRowDetailView'; import { ContainerService } from '../services/container.service'; // using external non-typed js libraries @@ -103,7 +88,7 @@ declare const Slick: SlickNamespace; // make everything transient (non-singleton) AngularUtilService, ApplicationRef, - RowDetailViewExtension, + SlickRowDetailView, TranslaterService, ] }) @@ -122,10 +107,11 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { private _isLocalGrid = true; private _paginationOptions: Pagination | undefined; private _registeredResources: ExternalResource[] = []; + private slickRowDetailView?: SlickRowDetailView; dataView!: SlickDataView; slickGrid!: SlickGrid; groupingDefinition: any = {}; - groupItemMetadataProvider: any; + groupItemMetadataProvider?: SlickGroupItemMetadataProvider; backendServiceApi?: BackendServiceApi; locales!: Locale; metrics?: Metrics; @@ -143,13 +129,11 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { slickFooter?: SlickFooterComponent; slickPagination?: SlickPaginationComponent; - // extensions - extensionUtility: ExtensionUtility; - // services backendUtilityService!: BackendUtilityService; collectionService: CollectionService; extensionService: ExtensionService; + extensionUtility: ExtensionUtility; filterFactory!: FilterFactory; filterService: FilterService; gridEventService: GridEventService; @@ -299,7 +283,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { this.gridEventService = externalServices?.gridEventService ?? new GridEventService(); this.sharedService = externalServices?.sharedService ?? new SharedService(); this.collectionService = externalServices?.collectionService ?? new CollectionService(this.translaterService); - this.extensionUtility = externalServices?.extensionUtility ?? new ExtensionUtility(this.sharedService, this.translaterService); + this.extensionUtility = externalServices?.extensionUtility ?? new ExtensionUtility(this.sharedService, this.backendUtilityService, this.translaterService); this.filterFactory = new FilterFactory(slickgridConfig, this.translaterService, this.collectionService); this.filterService = externalServices?.filterService ?? new FilterService(this.filterFactory as any, this._eventPubSubService, this.sharedService, this.backendUtilityService); this.resizerService = externalServices?.resizerService ?? new ResizerService(this._eventPubSubService); @@ -307,44 +291,19 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { this.treeDataService = externalServices?.treeDataService ?? new TreeDataService(this._eventPubSubService, this.sharedService, this.sortService); this.paginationService = externalServices?.paginationService ?? new PaginationService(this._eventPubSubService, this.sharedService, this.backendUtilityService); - // extensions - const autoTooltipExtension = new AutoTooltipExtension(this.sharedService); - const cellExternalCopyManagerExtension = new CellExternalCopyManagerExtension(this.extensionUtility, this.sharedService); - const cellMenuExtension = new CellMenuExtension(this.extensionUtility, this.sharedService, this.translaterService); - const contextMenuExtension = new ContextMenuExtension(this.extensionUtility, this._eventPubSubService, this.sharedService, this.treeDataService, this.translaterService); - const columnPickerExtension = new ColumnPickerExtension(this.extensionUtility, this.sharedService); - const checkboxExtension = new CheckboxSelectorExtension(this.sharedService); - const draggableGroupingExtension = new DraggableGroupingExtension(this.extensionUtility, this._eventPubSubService, this.sharedService); - const gridMenuExtension = new GridMenuExtension(this.extensionUtility, this.filterService, this.sharedService, this.sortService, this.backendUtilityService, this.translaterService); - const groupItemMetaProviderExtension = new GroupItemMetaProviderExtension(this.sharedService); - const headerButtonExtension = new HeaderButtonExtension(this.extensionUtility, this.sharedService); - const headerMenuExtension = new HeaderMenuExtension(this.extensionUtility, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.translaterService); - const rowDetailViewExtension = new RowDetailViewExtension(this.angularUtilService, this.appRef, this._eventPubSubService, this.sharedService, this.rxjs); - const rowMoveManagerExtension = new RowMoveManagerExtension(this.sharedService); - const rowSelectionExtension = new RowSelectionExtension(this.sharedService); - this.extensionService = externalServices?.extensionService ?? new ExtensionService( - autoTooltipExtension, - cellExternalCopyManagerExtension, - cellMenuExtension, - checkboxExtension, - columnPickerExtension, - contextMenuExtension, - draggableGroupingExtension, - gridMenuExtension, - groupItemMetaProviderExtension, - headerButtonExtension, - headerMenuExtension, - rowDetailViewExtension, - rowMoveManagerExtension, - rowSelectionExtension, + this.extensionUtility, + this.filterService, + this._eventPubSubService, this.sharedService, + this.sortService, + this.treeDataService, this.translaterService, ); this.gridStateService = externalServices?.gridStateService ?? new GridStateService(this.extensionService, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService); this.gridService = externalServices?.gridService ?? new GridService(this.gridStateService, this.filterService, this._eventPubSubService, this.paginationService, this.sharedService, this.sortService, this.treeDataService); - this.groupingService = externalServices?.groupingAndColspanService ?? new GroupingAndColspanService(this.extensionUtility, this.extensionService, this._eventPubSubService); + this.groupingService = externalServices?.groupingAndColspanService ?? new GroupingAndColspanService(this.extensionUtility, this._eventPubSubService); this.serviceList = [ this.extensionService, @@ -519,7 +478,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { let dataViewOptions: DataViewOption = { inlineFilters: dataviewInlineFilters }; if (this.gridOptions.draggableGrouping || this.gridOptions.enableGrouping) { - this.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider(); + this.groupItemMetadataProvider = new SlickGroupItemMetadataProvider(); this.sharedService.groupItemMetadataProvider = this.groupItemMetadataProvider; dataViewOptions = { ...dataViewOptions, groupItemMetadataProvider: this.groupItemMetadataProvider }; } @@ -556,6 +515,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { this.slickGrid = new Slick.Grid(`#${this.gridId}`, this.customDataView || this.dataView, this._columnDefinitions, this.gridOptions); this.sharedService.dataView = this.dataView; this.sharedService.slickGrid = this.slickGrid; + this.sharedService.gridContainerElement = this.elm.nativeElement as HTMLDivElement; this.extensionService.bindDifferentExtensions(); this.bindDifferentHooks(this.slickGrid, this.gridOptions, this.dataView); @@ -593,7 +553,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { // if you don't want the items that are not visible (due to being filtered out or being on a different page) // to stay selected, pass 'false' to the second arg - const selectionModel = this.slickGrid && this.slickGrid.getSelectionModel(); + const selectionModel = this.slickGrid?.getSelectionModel(); if (selectionModel && this.gridOptions && this.gridOptions.dataView && this.gridOptions.dataView.hasOwnProperty('syncGridSelection')) { // if we are using a Backend Service, we will do an extra flag check, the reason is because it might have some unintended behaviors // with the BackendServiceApi because technically the data in the page changes the DataView on every page change. @@ -833,6 +793,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { if (this.translate?.onLangChange) { // translate some of them on first load, then on each language change if (gridOptions.enableTranslate) { + this.extensionService.translateAllExtensions(); this.translateColumnHeaderTitleKeys(); this.translateColumnGroupKeys(); } @@ -843,12 +804,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { this._eventPubSubService.publish('onLanguageChange'); if (gridOptions.enableTranslate) { - this.extensionService.translateCellMenu(); - this.extensionService.translateColumnHeaders(); - this.extensionService.translateColumnPicker(); - this.extensionService.translateContextMenu(); - this.extensionService.translateGridMenu(); - this.extensionService.translateHeaderMenu(); + this.extensionService.translateAllExtensions(); this.translateColumnHeaderTitleKeys(); this.translateColumnGroupKeys(); if (gridOptions.createPreHeaderPanel && !gridOptions.enableDraggableGrouping) { @@ -874,9 +830,8 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { // expose all Slick Grid Events through dispatch for (const prop in grid) { if (grid.hasOwnProperty(prop) && prop.startsWith('on')) { - const gridEventHandler = (grid as any)[prop]; const gridEventName = this._eventPubSubService.getEventNameByNamingConvention(prop, slickgridEventPrefix); - (this._eventHandler as SlickEventHandler>).subscribe(gridEventHandler, (event, args) => { + this._eventHandler.subscribe((grid as any)[prop], (event, args) => { return this._eventPubSubService.dispatchCustomEvent(gridEventName, { eventData: event, args }); }); } @@ -885,8 +840,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { // expose all Slick DataView Events through dispatch for (const prop in dataView) { if (dataView.hasOwnProperty(prop) && prop.startsWith('on')) { - const dataViewEventHandler = (dataView as any)[prop]; - (this._eventHandler as SlickEventHandler>).subscribe(dataViewEventHandler, (event, args) => { + this._eventHandler.subscribe((dataView as any)[prop], (event, args) => { const dataViewEventName = this._eventPubSubService.getEventNameByNamingConvention(prop, slickgridEventPrefix); return this._eventPubSubService.dispatchCustomEvent(dataViewEventName, { eventData: event, args }); }); @@ -925,13 +879,11 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { this.loadFilterPresetsWhenDatasetInitialized(); // When data changes in the DataView, we need to refresh the metrics and/or display a warning if the dataset is empty - const onRowCountChangedHandler = dataView.onRowCountChanged; - (this._eventHandler as SlickEventHandler>).subscribe(onRowCountChangedHandler, (_e, args) => { + this._eventHandler.subscribe(dataView.onRowCountChanged, () => { grid.invalidate(); this.handleOnItemCountChanged(this.dataView.getFilteredItemCount() || 0, dataView.getItemCount()); }); - const onSetItemsCalledHandler = dataView.onSetItemsCalled; - (this._eventHandler as SlickEventHandler>).subscribe(onSetItemsCalledHandler, (_e, args) => { + this._eventHandler.subscribe(dataView.onSetItemsCalled, (_e, args) => { grid.invalidate(); this.handleOnItemCountChanged(this.dataView.getFilteredItemCount(), args.itemCount); @@ -941,8 +893,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { } }); - const onRowsChangedHandler = dataView.onRowsChanged; - (this._eventHandler as SlickEventHandler>).subscribe(onRowsChangedHandler, (_e, args) => { + this._eventHandler.subscribe(dataView.onRowsChanged, (_e, args) => { // filtering data with local dataset will not always show correctly unless we call this updateRow/render // also don't use "invalidateRows" since it destroys the entire row and as bad user experience when updating a row // see commit: https://github.com/ghiscoding/aurelia-slickgrid/commit/8c503a4d45fba11cbd8d8cc467fae8d177cc4f60 @@ -1196,7 +1147,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { private loadRowSelectionPresetWhenExists() { // if user entered some Row Selections "presets" const presets = this.gridOptions?.presets; - const selectionModel = this.slickGrid?.getSelectionModel?.(); + const selectionModel = this.slickGrid?.getSelectionModel(); const enableRowSelection = this.gridOptions && (this.gridOptions.enableCheckboxSelector || this.gridOptions.enableRowSelection); if (enableRowSelection && selectionModel && presets && presets.rowSelection && (Array.isArray(presets.rowSelection.gridRowIndexes) || Array.isArray(presets.rowSelection.dataContextIds))) { let dataContextIds = presets.rowSelection.dataContextIds; @@ -1292,6 +1243,13 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy { this.extensionService.translateColumnHeaders(); } + if (this.gridOptions.enableRowDetailView) { + this.slickRowDetailView = new SlickRowDetailView(this.angularUtilService, this.appRef, this._eventPubSubService, this.rxjs); + this.slickRowDetailView.create(this.columnDefinitions, this.gridOptions); + this._registeredResources.push(this.slickRowDetailView); + this.extensionService.addExtensionToList(ExtensionName.rowDetailView, { name: ExtensionName.rowDetailView, class: this.slickRowDetailView, instance: this.slickRowDetailView }); + } + // also initialize (render) the empty warning component this.slickEmptyWarning = new SlickEmptyWarningComponent(); this._registeredResources.push(this.slickEmptyWarning); diff --git a/src/app/modules/angular-slickgrid/extensions/__tests__/index.spec.ts b/src/app/modules/angular-slickgrid/extensions/__tests__/index.spec.ts new file mode 100644 index 000000000..3c92837f2 --- /dev/null +++ b/src/app/modules/angular-slickgrid/extensions/__tests__/index.spec.ts @@ -0,0 +1,11 @@ +import * as entry from '../index'; + +describe('Testing library entry point', () => { + it('should have an index entry point defined', () => { + expect(entry).toBeTruthy(); + }); + + it('should have all exported object defined', () => { + expect(typeof entry.SlickRowDetailView).toBe('function'); + }); +}); \ No newline at end of file diff --git a/src/app/modules/angular-slickgrid/extensions/__tests__/rowDetailViewExtension.spec.ts b/src/app/modules/angular-slickgrid/extensions/__tests__/rowDetailViewExtension.spec.ts deleted file mode 100644 index 0e0149d59..000000000 --- a/src/app/modules/angular-slickgrid/extensions/__tests__/rowDetailViewExtension.spec.ts +++ /dev/null @@ -1,674 +0,0 @@ -import { ApplicationRef, Component } from '@angular/core'; -import { TestBed, } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { Column, CurrentFilter, FilterService, SharedService, SlickEventHandler, SlickDataView, SlickGrid, SlickNamespace, SortService, SlickRowDetailView, RxJsFacade, } from '@slickgrid-universal/common'; -import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; -import { of, Subject } from 'rxjs'; - -import { GridOption } from '../../models/gridOption.interface'; -import { RowDetailViewExtension } from '../rowDetailViewExtension'; -import { AngularUtilService } from '../../services'; -import { RowDetailView } from '../../models/rowDetailView.interface'; -import { RxJsResourceStub } from '../../../../../../test/rxjsResourceStub'; - -declare const Slick: SlickNamespace; -const ROW_DETAIL_CONTAINER_PREFIX = 'container_'; -const PRELOAD_CONTAINER_PREFIX = 'container_loading'; - -const applicationRefStub = { - detachView: jest.fn(), -} as unknown as ApplicationRef; - -const angularUtilServiceStub = { - createAngularComponent: jest.fn(), - createAngularComponentAppendToDom: jest.fn(), -} as unknown as AngularUtilService; - -const filterServiceStub = { - clearFilters: jest.fn(), - onFilterChanged: new Subject(), -} as unknown as FilterService; - -const sortServiceStub = { - clearSorting: jest.fn(), -} as unknown as SortService; - -const dataViewStub = { - refresh: jest.fn(), -} as unknown as SlickDataView; - -const gridStub = { - getOptions: jest.fn(), - getSelectionModel: jest.fn(), - registerPlugin: jest.fn(), - setSelectionModel: jest.fn(), - onColumnsReordered: new Slick.Event(), - onSelectedRowsChanged: new Slick.Event(), - onSort: new Slick.Event(), -} as unknown as SlickGrid; - -const mockAddon = jest.fn().mockImplementation(() => ({ - init: jest.fn(), - destroy: jest.fn(), - getColumnDefinition: jest.fn(), - onAsyncResponse: new Slick.Event(), - onAsyncEndUpdate: new Slick.Event(), - onAfterRowDetailToggle: new Slick.Event(), - onBeforeRowDetailToggle: new Slick.Event(), - onRowOutOfViewportRange: new Slick.Event(), - onRowBackToViewportRange: new Slick.Event() -})); - -const mockSelectionModel = jest.fn().mockImplementation(() => ({ - init: jest.fn(), - destroy: jest.fn() -})); - -@Component({ - template: `

      Loading...

      ` -}) -export class TestPreloadComponent { } - -describe('rowDetailViewExtension', () => { - jest.mock('slickgrid/plugins/slick.rowdetailview', () => mockAddon); - Slick.Plugins = { - RowDetailView: mockAddon - } as any; - - jest.mock('slickgrid/plugins/slick.rowselectionmodel', () => mockSelectionModel); - Slick.RowSelectionModel = mockSelectionModel; - - let eventHandler: SlickEventHandler; - let extension: RowDetailViewExtension; - let eventPubSubService: EventPubSubService; - let rxjsResourceStub: RxJsResourceStub; - const div = document.createElement('div'); - div.innerHTML = `
      `; - document.body.appendChild(div); - - const gridOptionsMock = { - enableRowDetailView: true, - rowDetailView: { - cssClass: 'detail-view-toggle', - panelRows: 1, - keyPrefix: '__', - useRowClick: true, - useSimpleViewportCalc: true, - saveDetailViewOnScroll: false, - process: () => new Promise((resolve) => resolve('process resolved')), - viewComponent: null, - onExtensionRegistered: jest.fn(), - onAsyncResponse: (e: Event, args: { item: any; detailView?: any }) => { }, - onAsyncEndUpdate: (e: Event, args: { item: any; grid: SlickGrid; }) => { }, - onAfterRowDetailToggle: (e: Event, args: { item: any; expandedRows: any[]; grid: SlickGrid; }) => { }, - onBeforeRowDetailToggle: (e: Event, args: { item: any; grid: SlickGrid; }) => { }, - onRowOutOfViewportRange: (e: Event, args: { item: any; rowId: number; rowIndex: number; expandedRows: any[]; rowIdsOutOfViewport: number[]; grid: SlickGrid; }) => { }, - onRowBackToViewportRange: (e: Event, args: { item: any; rowId: number; rowIndex: number; expandedRows: any[]; rowIdsOutOfViewport: number[]; grid: SlickGrid; }) => { }, - } - } as unknown as GridOption; - - beforeEach(async () => { - eventHandler = new Slick.EventHandler(); - eventPubSubService = new EventPubSubService(div); - rxjsResourceStub = new RxJsResourceStub(); - - await TestBed.configureTestingModule({ - declarations: [TestPreloadComponent], - providers: [ - RowDetailViewExtension, - SharedService, - { provide: ApplicationRef, useValue: applicationRefStub }, - { provide: AngularUtilService, useValue: angularUtilServiceStub }, - { provide: EventPubSubService, useValue: eventPubSubService }, - { provide: FilterService, useValue: filterServiceStub }, - { provide: RxJsFacade, useValue: rxjsResourceStub }, - { provide: SortService, useValue: sortServiceStub }, - ], - imports: [TranslateModule.forRoot()] - }); - extension = TestBed.inject(RowDetailViewExtension); - }); - - afterEach(() => { - eventHandler.unsubscribeAll(); - jest.clearAllMocks(); - }); - - it('should return null after calling "create" method when either the column definitions or the grid options is missing', () => { - const output = extension.create([] as Column[], null as any); - expect(output).toBeNull(); - }); - - it('should return null after calling "register" method when either the grid object or the grid options is missing', () => { - const output = extension.register(); - expect(output).toBeNull(); - }); - - describe('create method', () => { - let columnsMock: Column[]; - - beforeEach(() => { - gridOptionsMock.datasetIdPropertyName = 'id'; - columnsMock = [ - { id: 'field1', field: 'field1', width: 100, cssClass: 'red' }, - { id: 'field2', field: 'field2', width: 50 } - ]; - jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); - jest.spyOn(SharedService.prototype, 'dataView', 'get').mockReturnValue(dataViewStub); - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - jest.clearAllMocks(); - }); - - it('should create the addon', () => { - extension.create(columnsMock, gridOptionsMock); - - expect(mockAddon).toHaveBeenCalledWith({ - cssClass: 'detail-view-toggle', - keyPrefix: '__', - panelRows: 1, - postTemplate: expect.anything(), - preTemplate: expect.anything(), - process: expect.anything(), - saveDetailViewOnScroll: false, - useRowClick: true, - useSimpleViewportCalc: true, - viewComponent: null, - onExtensionRegistered: expect.anything(), - onAsyncResponse: expect.anything(), - onAsyncEndUpdate: expect.anything(), - onAfterRowDetailToggle: expect.anything(), - onBeforeRowDetailToggle: expect.anything(), - onRowOutOfViewportRange: expect.anything(), - onRowBackToViewportRange: expect.anything(), - }); - }); - - it('should run the "process" method when defined', async () => { - (gridOptionsMock.rowDetailView as RowDetailView).process = () => new Promise((resolve) => resolve('process resolved')); - const output = await (gridOptionsMock.rowDetailView as RowDetailView).process({ id: 'field1', field: 'field1' }); - expect(output).toBe('process resolved'); - }); - - it('should use "addRxJsResource" method and run the "process" method when defined', async () => { - extension.addRxJsResource(rxjsResourceStub); - (gridOptionsMock.rowDetailView as RowDetailView).process = () => new Promise((resolve) => resolve('process resolved')); - const output = await (gridOptionsMock.rowDetailView as RowDetailView).process({ id: 'field1', field: 'field1' }); - expect(output).toBe('process resolved'); - }); - - it('should provide a sanitized "preTemplate" when only a "preloadComponent" is provided (meaning no "preTemplate" is originally provided)', async () => { - (gridOptionsMock.rowDetailView as RowDetailView).preloadComponent = TestPreloadComponent; - const output = await (gridOptionsMock.rowDetailView as RowDetailView).preTemplate!(); - expect(output).toEqual(`
      `); - }); - - it('should provide a sanitized "postTemplate" when only a "viewComponent" is provided (meaning no "postTemplate" is originally provided)', async () => { - (gridOptionsMock.rowDetailView as RowDetailView).viewComponent = TestPreloadComponent; - const output = await (gridOptionsMock.rowDetailView as RowDetailView).postTemplate!({ id: 'field1', field: 'field1' }); - expect(output).toEqual(`
      `); - }); - - it('should define "datasetIdPropertyName" with different "id" and provide a sanitized "postTemplate" when only a "viewComponent" is provided (meaning no "postTemplate" is originally provided)', async () => { - (gridOptionsMock.rowDetailView as RowDetailView).viewComponent = TestPreloadComponent; - gridOptionsMock.datasetIdPropertyName = 'rowId'; - const output = await (gridOptionsMock.rowDetailView as RowDetailView).postTemplate!({ rowId: 'field1', field: 'field1' }); - expect(output).toEqual(`
      `); - }); - - it('should add a reserved column for icons in 1st column index', () => { - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_detail_selector', field: 'sel' }); - extension.create(columnsMock, gridOptionsMock); - - expect(spy).toHaveBeenCalled(); - expect(columnsMock).toEqual([ - { - excludeFromColumnPicker: true, - excludeFromExport: true, - excludeFromGridMenu: true, - excludeFromHeaderMenu: true, - excludeFromQuery: true, - field: 'sel', - id: '_detail_selector' - }, - { id: 'field1', field: 'field1', width: 100, cssClass: 'red' }, - { id: 'field2', field: 'field2', width: 50 }, - ]); - }); - - it('should expect the column to be at a different column index position when "columnIndexPosition" is defined', () => { - (gridOptionsMock.rowDetailView as RowDetailView).columnIndexPosition = 2; - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_detail_selector', field: 'sel' }); - extension.create(columnsMock, gridOptionsMock); - - expect(spy).toHaveBeenCalled(); - expect(columnsMock).toEqual([ - { id: 'field1', field: 'field1', width: 100, cssClass: 'red' }, - { id: 'field2', field: 'field2', width: 50 }, - { - excludeFromColumnPicker: true, - excludeFromExport: true, - excludeFromGridMenu: true, - excludeFromHeaderMenu: true, - excludeFromQuery: true, - field: 'sel', - id: '_detail_selector' - }, - ]); - }); - }); - - describe('registered addon', () => { - let columnsMock: Column[]; - - beforeEach(() => { - (gridOptionsMock.rowDetailView as RowDetailView).preloadComponent = TestPreloadComponent; - (gridOptionsMock.rowDetailView as RowDetailView).viewComponent = TestPreloadComponent; - columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }]; - jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - jest.clearAllMocks(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should register the addon', () => { - const onRegisteredSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onExtensionRegistered'); - const pluginSpy = jest.spyOn(SharedService.prototype.slickGrid, 'registerPlugin'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - const addonInstance = extension.getAddonInstance(); - extension.register(); - - expect(instance).toBeTruthy(); - expect(instance).toEqual(addonInstance); - expect(onRegisteredSpy).toHaveBeenCalledWith(instance); - expect(mockSelectionModel).toHaveBeenCalledWith({ selectActiveRow: true }); - expect(pluginSpy).toHaveBeenCalledWith(instance); - }); - - it('should call internal event handler subscribe and expect the "onAsyncResponse" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - - const onAsyncRespSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncResponse'); - const onAsyncEndSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); - const onAfterRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); - const onBeforeRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); - const onRowOutViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); - const onRowBackViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - extension.register(); - instance.onAsyncResponse!.notify({ item: columnsMock[0], detailView: {} }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onAsyncRespSpy).toHaveBeenCalledWith(expect.anything(), { item: columnsMock[0], detailView: {} }); - expect(onAsyncEndSpy).not.toHaveBeenCalled(); - expect(onAfterRowSpy).not.toHaveBeenCalled(); - expect(onBeforeRowSpy).not.toHaveBeenCalled(); - expect(onRowOutViewSpy).not.toHaveBeenCalled(); - expect(onRowBackViewSpy).not.toHaveBeenCalled(); - }); - - it('should call internal event handler subscribe and expect the "onAsyncEndUpdate" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const renderSpy = jest.spyOn(extension, 'renderViewModel'); - - const onAsyncRespSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncResponse'); - const onAsyncEndSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); - const onAfterRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); - const onBeforeRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); - const onRowOutViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); - const onRowBackViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - extension.register(); - instance.onAsyncEndUpdate!.notify({ item: columnsMock[0], grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onAsyncRespSpy).not.toHaveBeenCalled(); - expect(onAsyncEndSpy).toHaveBeenCalledWith(expect.anything(), { item: columnsMock[0], grid: gridStub }); - expect(renderSpy).toHaveBeenCalledWith({ cssClass: 'red', field: 'field1', id: 'field1', width: 100, }); - expect(onAfterRowSpy).not.toHaveBeenCalled(); - expect(onBeforeRowSpy).not.toHaveBeenCalled(); - expect(onRowOutViewSpy).not.toHaveBeenCalled(); - expect(onRowBackViewSpy).not.toHaveBeenCalled(); - }); - - it('should call internal event handler subscribe and expect the "onAfterRowDetailToggle" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - - const onAsyncRespSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncResponse'); - const onAsyncEndSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); - const onAfterRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); - const onBeforeRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); - const onRowOutViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); - const onRowBackViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - extension.register(); - instance.onAfterRowDetailToggle!.notify({ item: columnsMock[0], expandedRows: [0], grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onAsyncRespSpy).not.toHaveBeenCalled(); - expect(onAsyncEndSpy).not.toHaveBeenCalled(); - expect(onAfterRowSpy).toHaveBeenCalledWith(expect.anything(), { item: columnsMock[0], expandedRows: [0], grid: gridStub }); - expect(onBeforeRowSpy).not.toHaveBeenCalled(); - expect(onRowOutViewSpy).not.toHaveBeenCalled(); - expect(onRowBackViewSpy).not.toHaveBeenCalled(); - }); - - it('should call internal event handler subscribe and expect the "onBeforeRowDetailToggle" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - - const onAsyncRespSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncResponse'); - const onAsyncEndSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); - const onAfterRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); - const onBeforeRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); - const onRowOutViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); - const onRowBackViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - extension.register(); - instance.onBeforeRowDetailToggle!.notify({ item: columnsMock[0], grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onAsyncRespSpy).not.toHaveBeenCalled(); - expect(onAsyncEndSpy).not.toHaveBeenCalled(); - expect(onAfterRowSpy).not.toHaveBeenCalled(); - expect(onBeforeRowSpy).toHaveBeenCalledWith(expect.anything(), { item: columnsMock[0], grid: gridStub }); - expect(onRowOutViewSpy).not.toHaveBeenCalled(); - expect(onRowBackViewSpy).not.toHaveBeenCalled(); - }); - - it('should call internal event handler subscribe and expect the "onRowOutOfViewportRange" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - - const onAsyncRespSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncResponse'); - const onAsyncEndSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); - const onAfterRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); - const onBeforeRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); - const onRowOutViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); - const onRowBackViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - extension.register(); - instance.onRowOutOfViewportRange!.notify( - { item: columnsMock[0], rowId: 0, rowIndex: 0, expandedRows: [0], rowIdsOutOfViewport: [], grid: gridStub }, - new Slick.EventData(), - gridStub - ); - - expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onAsyncRespSpy).not.toHaveBeenCalled(); - expect(onAsyncEndSpy).not.toHaveBeenCalled(); - expect(onAfterRowSpy).not.toHaveBeenCalled(); - expect(onBeforeRowSpy).not.toHaveBeenCalled(); - expect(onRowOutViewSpy).toHaveBeenCalledWith( - expect.anything(), { - item: columnsMock[0], rowId: 0, rowIndex: 0, expandedRows: [0], rowIdsOutOfViewport: [], grid: gridStub - }); - expect(onRowBackViewSpy).not.toHaveBeenCalled(); - }); - - it('should call internal event handler subscribe and expect the "onRowBackToViewportRange" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - - const onAsyncRespSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncResponse'); - const onAsyncEndSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); - const onAfterRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); - const onBeforeRowSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); - const onRowOutViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); - const onRowBackViewSpy = jest.spyOn(SharedService.prototype.gridOptions.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - extension.register(); - instance.onRowBackToViewportRange!.notify( - { item: columnsMock[0], rowId: 0, rowIndex: 0, expandedRows: [0], rowIdsOutOfViewport: [], grid: gridStub }, - new Slick.EventData(), - gridStub - ); - - expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onAsyncRespSpy).not.toHaveBeenCalled(); - expect(onAsyncEndSpy).not.toHaveBeenCalled(); - expect(onAfterRowSpy).not.toHaveBeenCalled(); - expect(onBeforeRowSpy).not.toHaveBeenCalled(); - expect(onRowOutViewSpy).not.toHaveBeenCalled(); - expect(onRowBackViewSpy).toHaveBeenCalledWith( - expect.anything(), { - item: columnsMock[0], rowId: 0, rowIndex: 0, expandedRows: [0], rowIdsOutOfViewport: [], grid: gridStub - }); - }); - - it('should call Angular Util "createAngularComponentAppendToDom" when grid "onColumnsReordered" is triggered', () => { - const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn() } } as any); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - extension.register(); - eventHandler.subscribe(instance.onBeforeRowDetailToggle!, () => { - gridStub.onColumnsReordered.notify({ impactedColumns: [mockColumn], grid: gridStub }, new Slick.EventData(), gridStub); - expect(appendSpy).toHaveBeenCalledWith(undefined, expect.objectContaining({ className: 'container_field1' }), true); - }); - instance.onBeforeRowDetailToggle!.notify({ item: mockColumn, grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalled(); - }); - - it('should call "redrawAllViewComponents" when using Row Selection and the event "onSelectedRowsChanged" is triggered', () => { - const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; - gridOptionsMock.enableCheckboxSelector = true; - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const redrawSpy = jest.spyOn(extension, 'redrawAllViewComponents'); - const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn(), destroy: jest.fn() } } as any); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - - extension.register(); - eventHandler.subscribe(instance.onBeforeRowDetailToggle!, () => { - gridStub.onSelectedRowsChanged.notify({ rows: [0], previousSelectedRows: [], grid: gridStub }, new Slick.EventData(), gridStub); - expect(appendSpy).toHaveBeenCalledWith(undefined, expect.objectContaining({ className: 'container_field1' }), true); - }); - instance.onBeforeRowDetailToggle!.notify({ item: mockColumn, grid: gridStub }, new Slick.EventData(), gridStub); - instance.onBeforeRowDetailToggle!.notify({ item: { ...mockColumn, __collapsed: false }, grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalled(); - expect(redrawSpy).toHaveBeenCalledTimes(2); - }); - - it('should call "redrawAllViewComponents" when event "filterChanged" is triggered', () => { - const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const redrawSpy = jest.spyOn(extension, 'redrawAllViewComponents'); - const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn(), destroy: jest.fn() } } as any); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - - extension.register(); - eventHandler.subscribe(instance.onBeforeRowDetailToggle!, () => { - eventPubSubService.publish('onFilterChanged', { columnId: 'field1', operator: '=', searchTerms: [] }); - expect(appendSpy).toHaveBeenCalledWith(undefined, expect.objectContaining({ className: 'container_field1' }), true); - }); - instance.onBeforeRowDetailToggle!.notify({ item: mockColumn, grid: gridStub }, new Slick.EventData(), gridStub); - instance.onBeforeRowDetailToggle!.notify({ item: { ...mockColumn, __collapsed: false }, grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalled(); - expect(redrawSpy).toHaveBeenCalledTimes(2); - }); - - it('should call "renderAllViewComponents" when grid event "onAfterRowDetailToggle" is triggered', (done) => { - const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const getElementSpy = jest.spyOn(document, 'getElementsByClassName'); - const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn(), destroy: jest.fn() } } as any); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - - extension.register(); - instance.onAfterRowDetailToggle!.subscribe(() => { - expect(getElementSpy).toHaveBeenCalledWith('container_field1'); - expect(appendSpy).toHaveBeenCalledWith(undefined, expect.objectContaining({ className: 'container_field1' }), true); - done(); - }); - instance.onBeforeRowDetailToggle!.notify({ item: mockColumn, grid: gridStub }, new Slick.EventData(), gridStub); - instance.onAfterRowDetailToggle!.notify({ item: mockColumn, expandedRows: [], grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalled(); - }); - - it('should call "redrawViewComponent" when grid event "onRowBackToViewportRange" is triggered', (done) => { - const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const getElementSpy = jest.spyOn(document, 'getElementsByClassName'); - const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn(), destroy: jest.fn() } } as any); - const renderSpy = jest.spyOn(extension, 'renderViewModel'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - - extension.register(); - instance.onRowBackToViewportRange!.subscribe(() => { - expect(getElementSpy).toHaveBeenCalledWith('container_field1'); - expect(appendSpy).toHaveBeenCalledWith(undefined, expect.objectContaining({ className: 'container_field1' }), true); - expect(renderSpy).toReturnWith( - expect.objectContaining({ - componentRef: { - destroy: expect.anything(), - instance: expect.objectContaining({ model: mockColumn, addon: expect.anything(), grid: gridStub, dataView: dataViewStub }) - } - }) - ); - done(); - }); - instance.onBeforeRowDetailToggle!.notify({ item: mockColumn, grid: gridStub }, new Slick.EventData(), gridStub); - instance.onRowBackToViewportRange!.notify({ item: mockColumn, rowId: 0, rowIndex: 0, expandedRows: [], rowIdsOutOfViewport: [], grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalled(); - }); - - it('should run the internal "onProcessing" and call "notifyTemplate" with a Promise when "process" method is defined and executed', (done) => { - const mockItem = { id: 2, firstName: 'John', lastName: 'Doe' }; - (gridOptionsMock.rowDetailView as RowDetailView).process = (item) => new Promise((resolve) => resolve(item)); - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - - instance.onAsyncResponse!.subscribe((_e: Event, response: any) => { - expect(response).toEqual(expect.objectContaining({ item: mockItem })); - done(); - }); - - (gridOptionsMock.rowDetailView as RowDetailView).process(mockItem); - }); - - it('should run the internal "onProcessing" and call "notifyTemplate" with an Object to simular HttpClient call when "process" method is defined and executed', (done) => { - const mockItem = { id: 2, firstName: 'John', lastName: 'Doe' }; - (gridOptionsMock.rowDetailView as RowDetailView).process = (item) => of(mockItem); - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - - instance.onAsyncResponse!.subscribe((_e: Event, response: any) => { - expect(response).toEqual(expect.objectContaining({ item: mockItem })); - done(); - }); - - (gridOptionsMock.rowDetailView as RowDetailView).process({ id: 'field1', field: 'field1' }); - }); - - it('should define "datasetIdPropertyName" with different "id" and run the internal "onProcessing" and call "notifyTemplate" with an Object to simular HttpClient call when "process" method is defined and executed', (done) => { - const mockItem = { rowId: 2, firstName: 'John', lastName: 'Doe' }; - (gridOptionsMock.rowDetailView as RowDetailView).process = (item) => of(mockItem); - gridOptionsMock.datasetIdPropertyName = 'rowId'; - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - - instance.onAsyncResponse!.subscribe((_e: Event, response: any) => { - expect(response).toEqual(expect.objectContaining({ item: mockItem })); - done(); - }); - - (gridOptionsMock.rowDetailView as RowDetailView).process({ rowId: 'field1', field: 'field1' }); - }); - - it('should run the internal "onProcessing" and call "notifyTemplate" with an Object to simular HttpClient call when "process" method is defined and executed', async () => { - const mockItem = { firstName: 'John', lastName: 'Doe' }; - (gridOptionsMock.rowDetailView as RowDetailView).process = (item) => new Promise((resolve) => resolve(item)); - extension.create(columnsMock, gridOptionsMock); - - try { - await (gridOptionsMock.rowDetailView as RowDetailView).process(mockItem); - } catch (e) { - expect(e.toString()).toContain(`[Angular-Slickgrid] could not process the Row Detail, you must make sure that your "process" callback`); - } - }); - - it('should call Angular Util "disposeAllViewComponents" when grid "onSort" is triggered', () => { - const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; - jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn(), destroy: jest.fn() } } as any); - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const disposeSpy = jest.spyOn(extension, 'disposeAllViewComponents'); - - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - extension.register(); - eventHandler.subscribe(instance.onBeforeRowDetailToggle!, () => { - gridStub.onSort.notify({ columnId: 'field1', sortCol: mockColumn, sortAsc: true }, new Slick.EventData(), gridStub); - expect(disposeSpy).toHaveBeenCalled(); - }); - instance.onBeforeRowDetailToggle!.notify({ item: mockColumn, grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalled(); - }); - - it('should dispose of the addon', () => { - const instance = extension.create(columnsMock, gridOptionsMock) as SlickRowDetailView; - extension.register(); - const destroySpy = jest.spyOn(instance, 'destroy'); - - extension.dispose(); - - expect(destroySpy).toHaveBeenCalled(); - }); - }); - - describe('possible error thrown', () => { - it('should throw an error when calling "create" method without "rowDetailView" options defined', () => { - const copyGridOptionsMock = { ...gridOptionsMock }; - copyGridOptionsMock.rowDetailView = undefined; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - - expect(() => extension.create([], copyGridOptionsMock)).toThrowError(`The Row Detail View requires options to be passed via the "rowDetailView" property of the Grid Options`); - }); - - it('should throw an error when calling "create" method without "rowDetailView" options defined', () => { - const copyGridOptionsMock = { ...gridOptionsMock }; - (copyGridOptionsMock.rowDetailView as RowDetailView).process = undefined as any; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - - expect(() => extension.create([], copyGridOptionsMock)).toThrowError(`You need to provide a "process" function for the Row Detail Extension to work properly`); - }); - }); -}); diff --git a/src/app/modules/angular-slickgrid/extensions/__tests__/slickRowDetailView.spec.ts b/src/app/modules/angular-slickgrid/extensions/__tests__/slickRowDetailView.spec.ts new file mode 100644 index 000000000..566282d68 --- /dev/null +++ b/src/app/modules/angular-slickgrid/extensions/__tests__/slickRowDetailView.spec.ts @@ -0,0 +1,611 @@ +import { ApplicationRef, Component } from '@angular/core'; +import { TestBed, } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { Column, SharedService, SlickEventHandler, SlickGrid, SlickNamespace, RxJsFacade, } from '@slickgrid-universal/common'; +import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; +import { SlickRowSelectionModel } from '@slickgrid-universal/common/dist/commonjs/extensions/slickRowSelectionModel'; +import { SlickRowDetailView as UniversalSlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin'; +import { of } from 'rxjs'; + +import { GridOption } from '../../models/gridOption.interface'; +import { AngularUtilService } from '../../services'; +import { RowDetailView } from '../../models/rowDetailView.interface'; +import { RxJsResourceStub } from '../../../../../../test/rxjsResourceStub'; +import { SlickRowDetailView } from '../slickRowDetailView'; +jest.mock('@slickgrid-universal/row-detail-view-plugin'); + +declare const Slick: SlickNamespace; +const ROW_DETAIL_CONTAINER_PREFIX = 'container_'; +const PRELOAD_CONTAINER_PREFIX = 'container_loading'; + +const applicationRefStub = { + detachView: jest.fn(), +} as unknown as ApplicationRef; + +const angularUtilServiceStub = { + createAngularComponent: jest.fn(), + createAngularComponentAppendToDom: jest.fn(), +} as unknown as AngularUtilService; + +const gridOptionsMock = { + enableRowDetailView: true, + rowDetailView: { + cssClass: 'detail-view-toggle', + panelRows: 1, + keyPrefix: '__', + useRowClick: true, + useSimpleViewportCalc: true, + saveDetailViewOnScroll: false, + process: () => new Promise((resolve) => resolve('process resolved')), + viewComponent: null, + onExtensionRegistered: jest.fn(), + onAsyncResponse: () => { }, + onAsyncEndUpdate: () => { }, + onAfterRowDetailToggle: () => { }, + onBeforeRowDetailToggle: () => { }, + onRowOutOfViewportRange: () => { }, + onRowBackToViewportRange: () => { }, + } +} as unknown as GridOption; + +const gridStub = { + getUID: jest.fn(), + getOptions: () => gridOptionsMock, + getSelectionModel: jest.fn(), + setSelectionModel: jest.fn(), + onColumnsReordered: new Slick.Event(), + onSelectedRowsChanged: new Slick.Event(), + onSort: new Slick.Event(), +} as unknown as SlickGrid; + +const mockRowSelectionModel = { + constructor: jest.fn(), + init: jest.fn(), + destroy: jest.fn(), + dispose: jest.fn(), + getSelectedRows: jest.fn(), + setSelectedRows: jest.fn(), + getSelectedRanges: jest.fn(), + setSelectedRanges: jest.fn(), + onSelectedRangesChanged: new Slick.Event(), +} as unknown as SlickRowSelectionModel; + +jest.mock('@slickgrid-universal/common/dist/commonjs/extensions/slickRowSelectionModel', () => ({ + SlickRowSelectionModel: jest.fn().mockImplementation(() => mockRowSelectionModel), +})); + +@Component({ + template: `

      Loading...

      ` +}) +export class TestPreloadComponent { } + +describe('SlickRowDetailView', () => { + let eventHandler: SlickEventHandler; + let plugin: SlickRowDetailView; + let eventPubSubService: EventPubSubService; + let rxjsResourceStub: RxJsResourceStub; + const div = document.createElement('div'); + div.innerHTML = `
      `; + document.body.appendChild(div); + + beforeEach(async () => { + eventHandler = new Slick.EventHandler(); + eventPubSubService = new EventPubSubService(div); + rxjsResourceStub = new RxJsResourceStub(); + + await TestBed.configureTestingModule({ + declarations: [TestPreloadComponent], + providers: [ + SlickRowDetailView, + UniversalSlickRowDetailView, + { provide: ApplicationRef, useValue: applicationRefStub }, + { provide: AngularUtilService, useValue: angularUtilServiceStub }, + { provide: EventPubSubService, useValue: eventPubSubService }, + { provide: RxJsFacade, useValue: rxjsResourceStub }, + ], + imports: [TranslateModule.forRoot()] + }); + plugin = TestBed.inject(SlickRowDetailView); + plugin.eventHandler = new Slick.EventHandler(); + }); + + afterEach(() => { + eventHandler.unsubscribeAll(); + jest.clearAllMocks(); + }); + + it('should create the RowDetailView plugin', () => { + expect(plugin).toBeTruthy(); + }); + + it('should expect "getOptions" to be called when calling addonOptions GETTER', () => { + const getOptionSpy = jest.spyOn(plugin, 'getOptions').mockReturnValue({ cssClass: 'some-class' } as any); + + const options = plugin.addonOptions; + + expect(options).toEqual({ cssClass: 'some-class' }); + expect(getOptionSpy).toHaveBeenCalled(); + }); + + describe('registered plugin', () => { + beforeEach(() => { + gridOptionsMock.datasetIdPropertyName = 'id'; + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should run the "process" method when defined', async () => { + gridOptionsMock.rowDetailView!.process = () => new Promise((resolve) => resolve('process resolved')); + const output = await gridOptionsMock.rowDetailView!.process({ id: 'field1', field: 'field1' }); + expect(output).toBe('process resolved'); + }); + + it('should use "addRxJsResource" method and run the "process" method when defined', async () => { + plugin.addRxJsResource(rxjsResourceStub); + (gridOptionsMock.rowDetailView as RowDetailView).process = () => new Promise((resolve) => resolve('process resolved')); + const output = await (gridOptionsMock.rowDetailView as RowDetailView).process({ id: 'field1', field: 'field1' }); + expect(output).toBe('process resolved'); + }); + + it('should provide a sanitized "preTemplate" when only a "preloadComponent" is provided (meaning no "preTemplate" is originally provided)', async () => { + (gridOptionsMock.rowDetailView as RowDetailView).preloadComponent = TestPreloadComponent; + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + + plugin.init(gridStub); + const output = await (gridOptionsMock.rowDetailView as RowDetailView).preTemplate!(); + + expect(output).toEqual(`
      `); + }); + + it('should provide a sanitized "postTemplate" when only a "viewComponent" is provided (meaning no "postTemplate" is originally provided)', async () => { + (gridOptionsMock.rowDetailView as RowDetailView).viewComponent = TestPreloadComponent; + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + + const output = await gridOptionsMock.rowDetailView!.postTemplate!({ id: 'field1', field: 'field1' }); + expect(output).toEqual(`
      `); + }); + + it('should define "datasetIdPropertyName" with different "id" and provide a sanitized "postTemplate" when only a "viewComponent" is provided (meaning no "postTemplate" is originally provided)', async () => { + (gridOptionsMock.rowDetailView as RowDetailView).viewComponent = TestPreloadComponent; + gridOptionsMock.datasetIdPropertyName = 'rowId'; + const output = await gridOptionsMock.rowDetailView!.postTemplate!({ rowId: 'field1', field: 'field1' }); + expect(output).toEqual(`
      `); + }); + + describe('registered addon', () => { + let columnsMock: Column[]; + + beforeEach(() => { + gridOptionsMock.datasetIdPropertyName = 'id'; + gridOptionsMock.rowDetailView!.preloadComponent = TestPreloadComponent; + gridOptionsMock.rowDetailView!.viewComponent = TestPreloadComponent; + columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }]; + jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + jest.clearAllMocks(); + gridStub.onColumnsReordered = new Slick.Event(); + gridStub.onSort = new Slick.Event(); + }); + + afterEach(() => { + plugin?.eventHandler?.unsubscribeAll(); + plugin?.dispose(); + jest.clearAllMocks(); + (plugin.onAsyncResponse as any) = null; + (plugin.onAsyncEndUpdate as any) = null; + (plugin.onAfterRowDetailToggle as any) = null; + (plugin.onBeforeRowDetailToggle as any) = null; + (plugin.onRowBackToViewportRange as any) = null; + (plugin.onRowOutOfViewportRange as any) = null; + }); + + it('should register the addon', () => { + const copyGridOptionsMock = { ...gridOptionsMock }; + gridOptionsMock.rowDetailView!.onExtensionRegistered = jest.fn(); + jest.spyOn(gridStub, 'getOptions').mockReturnValue(copyGridOptionsMock); + const onRegisteredSpy = jest.spyOn(copyGridOptionsMock.rowDetailView!, 'onExtensionRegistered'); + + plugin.init(gridStub); + const instance = plugin.register(); + const addonInstance = plugin.getAddonInstance(); + + expect(instance).toBeTruthy(); + expect(instance).toEqual(addonInstance); + expect(onRegisteredSpy).toHaveBeenCalledWith(instance); + expect(SlickRowSelectionModel).toHaveBeenCalledWith({ selectActiveRow: true }); + }); + + it('should call internal event handler subscribe and expect the "onAsyncResponse" option to be called when addon notify is called', () => { + const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const onAsyncRespSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncResponse'); + const onAsyncEndSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); + const onAfterRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); + const onBeforeRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); + const onRowOutViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); + const onRowBackViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); + + plugin.init(gridStub); + plugin.onAsyncResponse = new Slick.Event(); + plugin.register(); + + plugin.onAsyncResponse.notify({ item: columnsMock[0], detailView: {} }, new Slick.EventData(), gridStub); + + // expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself + expect(handlerSpy).toHaveBeenCalledWith( + { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + expect.anything() + ); + expect(onAsyncRespSpy).toHaveBeenCalledWith(expect.anything(), { item: columnsMock[0], detailView: {} }); + expect(onAsyncEndSpy).not.toHaveBeenCalled(); + expect(onAfterRowSpy).not.toHaveBeenCalled(); + expect(onBeforeRowSpy).not.toHaveBeenCalled(); + expect(onRowOutViewSpy).not.toHaveBeenCalled(); + expect(onRowBackViewSpy).not.toHaveBeenCalled(); + }); + + it('should call internal event handler subscribe and expect the "onAsyncEndUpdate" option to be called when addon notify is called', () => { + // const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const renderSpy = jest.spyOn(plugin, 'renderViewModel'); + + const onAsyncRespSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncResponse'); + const onAsyncEndSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); + const onAfterRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); + const onBeforeRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); + const onRowOutViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); + const onRowBackViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); + + plugin.init(gridStub); + plugin.onAsyncEndUpdate = new Slick.Event(); + plugin.register(); + plugin.onAsyncEndUpdate.notify({ item: columnsMock[0], grid: gridStub }, new Slick.EventData(), gridStub); + + // expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself + // expect(handlerSpy).toHaveBeenCalledWith( + // { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + // expect.anything() + // ); + expect(onAsyncRespSpy).not.toHaveBeenCalled(); + expect(onAsyncEndSpy).toHaveBeenCalledWith(expect.anything(), { item: columnsMock[0], grid: gridStub }); + expect(renderSpy).toHaveBeenCalledWith({ cssClass: 'red', field: 'field1', id: 'field1', width: 100, }); + expect(onAfterRowSpy).not.toHaveBeenCalled(); + expect(onBeforeRowSpy).not.toHaveBeenCalled(); + expect(onRowOutViewSpy).not.toHaveBeenCalled(); + expect(onRowBackViewSpy).not.toHaveBeenCalled(); + }); + + it('should call internal event handler subscribe and expect the "onAfterRowDetailToggle" option to be called when addon notify is called', () => { + // const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const onAsyncRespSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncResponse'); + const onAsyncEndSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); + const onAfterRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); + const onBeforeRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); + const onRowOutViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); + const onRowBackViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); + + plugin.init(gridStub); + plugin.onAfterRowDetailToggle = new Slick.Event(); + plugin.register(); + plugin.onAfterRowDetailToggle.notify({ item: columnsMock[0], expandedRows: [0], grid: gridStub }, new Slick.EventData(), gridStub); + + // expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself + // expect(handlerSpy).toHaveBeenCalledWith( + // { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + // expect.anything() + // ); + expect(onAsyncRespSpy).not.toHaveBeenCalled(); + expect(onAsyncEndSpy).not.toHaveBeenCalled(); + expect(onAfterRowSpy).toHaveBeenCalledWith(expect.anything(), { item: columnsMock[0], expandedRows: [0], grid: gridStub }); + expect(onBeforeRowSpy).not.toHaveBeenCalled(); + expect(onRowOutViewSpy).not.toHaveBeenCalled(); + expect(onRowBackViewSpy).not.toHaveBeenCalled(); + }); + + it('should call internal event handler subscribe and expect the "onBeforeRowDetailToggle" option to be called when addon notify is called', () => { + // const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const onAsyncRespSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncResponse'); + const onAsyncEndSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); + const onAfterRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); + const onBeforeRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); + const onRowOutViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); + const onRowBackViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); + + plugin.init(gridStub); + plugin.onBeforeRowDetailToggle = new Slick.Event(); + plugin.register(); + plugin.onBeforeRowDetailToggle.notify({ item: columnsMock[0], grid: gridStub }, new Slick.EventData(), gridStub); + + // expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself + // expect(handlerSpy).toHaveBeenCalledWith( + // { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + // expect.anything() + // ); + expect(onAsyncRespSpy).not.toHaveBeenCalled(); + expect(onAsyncEndSpy).not.toHaveBeenCalled(); + expect(onAfterRowSpy).not.toHaveBeenCalled(); + expect(onBeforeRowSpy).toHaveBeenCalledWith(expect.anything(), { item: columnsMock[0], grid: gridStub }); + expect(onRowOutViewSpy).not.toHaveBeenCalled(); + expect(onRowBackViewSpy).not.toHaveBeenCalled(); + }); + + it('should call internal event handler subscribe and expect the "onRowOutOfViewportRange" option to be called when addon notify is called', () => { + // const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const onAsyncRespSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncResponse'); + const onAsyncEndSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); + const onAfterRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); + const onBeforeRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); + const onRowOutViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); + const onRowBackViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); + + plugin.init(gridStub); + plugin.onRowOutOfViewportRange = new Slick.Event(); + plugin.register(); + plugin.onRowOutOfViewportRange.notify( + { item: columnsMock[0], rowId: 0, rowIndex: 0, expandedRows: [0], rowIdsOutOfViewport: [], grid: gridStub }, + new Slick.EventData(), + gridStub + ); + + // expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself + // expect(handlerSpy).toHaveBeenCalledWith( + // { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + // expect.anything() + // ); + expect(onAsyncRespSpy).not.toHaveBeenCalled(); + expect(onAsyncEndSpy).not.toHaveBeenCalled(); + expect(onAfterRowSpy).not.toHaveBeenCalled(); + expect(onBeforeRowSpy).not.toHaveBeenCalled(); + expect(onRowOutViewSpy).toHaveBeenCalledWith(expect.anything(), { item: columnsMock[0], rowId: 0, rowIndex: 0, expandedRows: [0], rowIdsOutOfViewport: [], grid: gridStub }); + expect(onRowBackViewSpy).not.toHaveBeenCalled(); + }); + + it('should call internal event handler subscribe and expect the "onRowBackToViewportRange" option to be called when addon notify is called', () => { + // const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const onAsyncRespSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncResponse'); + const onAsyncEndSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAsyncEndUpdate'); + const onAfterRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onAfterRowDetailToggle'); + const onBeforeRowSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onBeforeRowDetailToggle'); + const onRowOutViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowOutOfViewportRange'); + const onRowBackViewSpy = jest.spyOn(gridOptionsMock.rowDetailView as RowDetailView, 'onRowBackToViewportRange'); + + plugin.init(gridStub); + plugin.onRowBackToViewportRange = new Slick.Event(); + plugin.register(); + plugin.onRowBackToViewportRange.notify( + { item: columnsMock[0], rowId: 0, rowIndex: 0, expandedRows: [columnsMock[0] as any], rowIdsOutOfViewport: [], grid: gridStub }, + new Slick.EventData(), + gridStub + ); + + // expect(handlerSpy).toHaveBeenCalledTimes(8); // there are an extra 2x on the grid itself + // expect(handlerSpy).toHaveBeenCalledWith( + // { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + // expect.anything() + // ); + expect(onAsyncRespSpy).not.toHaveBeenCalled(); + expect(onAsyncEndSpy).not.toHaveBeenCalled(); + expect(onAfterRowSpy).not.toHaveBeenCalled(); + expect(onBeforeRowSpy).not.toHaveBeenCalled(); + expect(onRowOutViewSpy).not.toHaveBeenCalled(); + expect(onRowBackViewSpy).toHaveBeenCalledWith(expect.anything(), { item: columnsMock[0], rowId: 0, rowIndex: 0, expandedRows: [columnsMock[0] as any], rowIdsOutOfViewport: [], grid: gridStub }); + }); + + it('should call Angular Util "createAngularComponentAppendToDom" when grid "onColumnsReordered" is triggered', (done) => { + const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; + const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn() } } as any); + + plugin.init(gridStub); + plugin.onBeforeRowDetailToggle = new Slick.Event(); + plugin.register(); + plugin.eventHandler.subscribe(plugin.onBeforeRowDetailToggle, () => { + gridStub.onColumnsReordered.notify({ impactedColumns: [mockColumn] } as any, new Slick.EventData(), gridStub); + expect(appendSpy).toHaveBeenCalledWith(undefined, expect.objectContaining({ className: 'container_field1' }), true); + done(); + }); + plugin.onBeforeRowDetailToggle.notify({ item: mockColumn, grid: gridStub }, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalled(); + }); + + it('should call "redrawAllViewComponents" when using Row Selection and the event "onSelectedRowsChanged" is triggered', () => { + const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; + gridOptionsMock.enableCheckboxSelector = true; + const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn(), destroy: jest.fn() } } as any); + + plugin.init(gridStub); + const redrawSpy = jest.spyOn(plugin, 'redrawAllViewComponents'); + plugin.onBeforeRowDetailToggle = new Slick.Event(); + plugin.register(); + plugin.eventHandler.subscribe(plugin.onBeforeRowDetailToggle, () => { + gridStub.onSelectedRowsChanged.notify({ rows: [0], previousSelectedRows: [], grid: gridStub }, new Slick.EventData(), gridStub); + expect(appendSpy).toHaveBeenCalledWith(undefined, expect.objectContaining({ className: 'container_field1' }), true); + }); + plugin.onBeforeRowDetailToggle.notify({ item: mockColumn, grid: gridStub }, new Slick.EventData(), gridStub); + plugin.onBeforeRowDetailToggle.notify({ item: { ...mockColumn, __collapsed: false }, grid: gridStub }, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalled(); + expect(redrawSpy).toHaveBeenCalledTimes(2); + }); + + it('should call "redrawAllViewSlots" when event "filterChanged" is triggered', () => { + const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; + const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn(), destroy: jest.fn() } } as any); + + plugin.init(gridStub); + const redrawSpy = jest.spyOn(plugin, 'redrawAllViewComponents'); + plugin.onBeforeRowDetailToggle = new Slick.Event(); + plugin.register(); + + plugin.eventHandler.subscribe(plugin.onBeforeRowDetailToggle, () => { + eventPubSubService.publish('onFilterChanged', { columnId: 'field1', operator: '=', searchTerms: [] }); + expect(appendSpy).toHaveBeenCalledWith(undefined, expect.objectContaining({ className: 'container_field1' }), true); + }); + plugin.onBeforeRowDetailToggle.notify({ item: mockColumn, grid: gridStub }, new Slick.EventData(), gridStub); + plugin.onBeforeRowDetailToggle.notify({ item: { ...mockColumn, __collapsed: false }, grid: gridStub }, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalled(); + expect(redrawSpy).toHaveBeenCalledTimes(2); + }); + + it('should call "renderAllViewModels" when grid event "onAfterRowDetailToggle" is triggered', () => { + const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; + const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const getElementSpy = jest.spyOn(document, 'getElementsByClassName'); + const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn(), destroy: jest.fn() } } as any); + + plugin.init(gridStub); + plugin.onAfterRowDetailToggle = new Slick.Event(); + plugin.onBeforeRowDetailToggle = new Slick.Event(); + plugin.register(); + plugin.onAfterRowDetailToggle.subscribe(() => { + expect(getElementSpy).toHaveBeenCalledWith('container_field1'); + expect(appendSpy).toHaveBeenCalledWith(undefined, expect.objectContaining({ className: 'container_field1' }), true); + }); + plugin.onBeforeRowDetailToggle.notify({ item: mockColumn, grid: gridStub } as any, new Slick.EventData(), gridStub); + plugin.onAfterRowDetailToggle.notify({ item: mockColumn, grid: gridStub } as any, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalled(); + }); + + it('should call "redrawViewSlot" when grid event "onRowBackToViewportRange" is triggered', (done) => { + const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; + const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const getElementSpy = jest.spyOn(document, 'getElementsByClassName'); + const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn(), destroy: jest.fn() } } as any); + + plugin.init(gridStub); + plugin.onBeforeRowDetailToggle = new Slick.Event(); + plugin.onRowBackToViewportRange = new Slick.Event(); + plugin.register(); + plugin.onRowBackToViewportRange.subscribe(() => { + expect(getElementSpy).toHaveBeenCalledWith('container_field1'); + expect(appendSpy).toHaveBeenCalledWith(undefined, expect.objectContaining({ className: 'container_field1' }), true); + done(); + }); + plugin.onBeforeRowDetailToggle.notify({ item: mockColumn, grid: gridStub } as any, new Slick.EventData(), gridStub); + plugin.onRowBackToViewportRange.notify({ item: mockColumn, grid: gridStub } as any, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalled(); + }); + + it('should run the internal "onProcessing" and call "notifyTemplate" with a Promise when "process" method is defined and executed', (done) => { + const mockItem = { id: 2, firstName: 'John', lastName: 'Doe' }; + gridOptionsMock.rowDetailView!.process = () => new Promise((resolve) => resolve(mockItem)); + plugin.init(gridStub); + plugin.onAsyncResponse = new Slick.Event(); + plugin.onAsyncResponse.subscribe((_e, response) => { + expect(response).toEqual(expect.objectContaining({ item: mockItem })); + done(); + }); + + gridOptionsMock.rowDetailView!.process(mockItem); + }); + + it('should run the internal "onProcessing" and call "notifyTemplate" with an Object to simular HttpClient call when "process" method is defined and executed', (done) => { + const mockItem = { id: 2, firstName: 'John', lastName: 'Doe' }; + (gridOptionsMock.rowDetailView as RowDetailView).process = (item) => of(mockItem); + + plugin.init(gridStub); + plugin.onAsyncResponse = new Slick.Event(); + plugin.onAsyncResponse.subscribe((_e: Event, response: any) => { + expect(response).toEqual(expect.objectContaining({ item: mockItem })); + done(); + }); + + (gridOptionsMock.rowDetailView as RowDetailView).process({ id: 'field1', field: 'field1' }); + }); + + it('should define "datasetIdPropertyName" with different "id" and run the internal "onProcessing" and call "notifyTemplate" with an Object to simular HttpClient call when "process" method is defined and executed', (done) => { + const mockItem = { rowId: 2, firstName: 'John', lastName: 'Doe' }; + (gridOptionsMock.rowDetailView as RowDetailView).process = (item) => of(mockItem); + gridOptionsMock.datasetIdPropertyName = 'rowId'; + + plugin.init(gridStub); + plugin.onAsyncResponse = new Slick.Event(); + plugin.onAsyncResponse.subscribe((_e: Event, response: any) => { + expect(response).toEqual(expect.objectContaining({ item: mockItem })); + done(); + }); + + (gridOptionsMock.rowDetailView as RowDetailView).process({ rowId: 'field1', field: 'field1' }); + }); + + it('should run the internal "onProcessing" and call "notifyTemplate" with an Object to simular HttpClient call when "process" method is defined and executed', async () => { + const mockItem = { firstName: 'John', lastName: 'Doe' }; + (gridOptionsMock.rowDetailView as RowDetailView).process = (item) => new Promise((resolve) => resolve(item)); + plugin.init(gridStub); + + try { + await (gridOptionsMock.rowDetailView as RowDetailView).process(mockItem); + } catch (e) { + expect(e.toString()).toContain(`[Angular-Slickgrid] could not process the Row Detail, you must make sure that your "process" callback`); + } + }); + + it('should throw an error when running the "process" that does not return an object with an "id" property', async () => { + const mockItem = { firstName: 'John', lastName: 'Doe' }; + gridOptionsMock.rowDetailView!.process = (item) => new Promise((resolve) => resolve(item)); + plugin.init(gridStub); + + try { + await gridOptionsMock.rowDetailView!.process(mockItem); + } catch (e) { + expect(e.toString()).toContain(`[Angular-Slickgrid] could not process the Row Detail, you must make sure that your "process" callback`); + } + }); + + it('should call Aurelia Util "disposeAllViewSlot" when grid "onSort" is triggered', (done) => { + const mockColumn = { id: 'field1', field: 'field1', width: 100, cssClass: 'red', __collapsed: true }; + // const appendSpy = jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { instance: jest.fn(), destroy: jest.fn() } } as any); + jest.spyOn(angularUtilServiceStub, 'createAngularComponentAppendToDom').mockReturnValue({ componentRef: { plugin: {} } } as any); + const handlerSpy = jest.spyOn(plugin.eventHandler, 'subscribe'); + const disposeSpy = jest.spyOn(plugin, 'disposeAllViewComponents'); + + plugin.init(gridStub); + plugin.onBeforeRowDetailToggle = new Slick.Event(); + plugin.eventHandler.subscribe(plugin.onBeforeRowDetailToggle, () => { + gridStub.onSort.notify({ impactedColumns: [mockColumn] } as any, new Slick.EventData(), gridStub); + expect(disposeSpy).toHaveBeenCalled(); + done(); + }); + plugin.onBeforeRowDetailToggle.notify({ item: mockColumn, grid: gridStub }, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalled(); + }); + + it('should dispose of the addon', () => { + plugin.init(gridStub); + const disposeSpy = jest.spyOn(plugin, 'dispose'); + const disposeAllSpy = jest.spyOn(plugin, 'disposeAllViewComponents'); + + plugin.dispose(); + + expect(disposeSpy).toHaveBeenCalled(); + expect(disposeAllSpy).toHaveBeenCalled(); + }); + }); + + describe('possible error thrown', () => { + it('should throw an error when creating with "init" and the row detail is without a "process" method defined', () => { + const copyGridOptionsMock = { ...gridOptionsMock }; + copyGridOptionsMock.rowDetailView!.process = undefined as any; + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + + expect(() => plugin.init(gridStub)).toThrowError(`[Angular-Slickgrid] You need to provide a "process" function for the Row Detail Extension to work properly`); + }); + + it('should throw an error when creating with "register" and the row detail is without a "process" method defined', () => { + const copyGridOptionsMock = { ...gridOptionsMock }; + copyGridOptionsMock.rowDetailView!.process = undefined as any; + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + + expect(() => plugin.register()).toThrowError(`[Angular-Slickgrid] You need to provide a "process" function for the Row Detail Extension to work properly`); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/app/modules/angular-slickgrid/extensions/index.ts b/src/app/modules/angular-slickgrid/extensions/index.ts index e35fe9f91..5da5386a6 100644 --- a/src/app/modules/angular-slickgrid/extensions/index.ts +++ b/src/app/modules/angular-slickgrid/extensions/index.ts @@ -1 +1 @@ -export * from './rowDetailViewExtension'; +export * from './slickRowDetailView'; diff --git a/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts b/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts deleted file mode 100644 index 47d0d5d6e..000000000 --- a/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts +++ /dev/null @@ -1,401 +0,0 @@ -import 'slickgrid/plugins/slick.rowdetailview'; -import 'slickgrid/plugins/slick.rowselectionmodel'; - -import { ApplicationRef, ComponentRef, Injectable, Type, ViewContainerRef } from '@angular/core'; -import { - addToArrayWhenNotExists, - castObservableToPromise, - Column, - RowDetailViewExtension as UniversalRowDetailViewExtension, - RxJsFacade, - SharedService, - SlickEventHandler, - SlickGrid, - SlickNamespace, - SlickRowDetailView, -} from '@slickgrid-universal/common'; -import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; -import { Observable, Subject, Subscription } from 'rxjs'; -import * as DOMPurify_ from 'dompurify'; -const DOMPurify = DOMPurify_; // patch to fix rollup to work - -import { GridOption, RowDetailView } from '../models/index'; -import { AngularUtilService } from '../services/angularUtil.service'; -import { unsubscribeAllObservables } from '../services/utilities'; - -// using external non-typed js libraries -declare const Slick: SlickNamespace; - -const ROW_DETAIL_CONTAINER_PREFIX = 'container_'; -const PRELOAD_CONTAINER_PREFIX = 'container_loading'; - -export interface CreatedView { - id: string | number; - dataContext: any; - componentRef?: ComponentRef; -} - -@Injectable() -export class RowDetailViewExtension implements UniversalRowDetailViewExtension { - rowDetailContainer!: ViewContainerRef; - private _addon: any; - private _addonOptions!: RowDetailView | null; - private _eventHandler: SlickEventHandler; - private _preloadComponent: Type | undefined; - private _views: CreatedView[] = []; - private _viewComponent!: Type; - private _subscriptions: Subscription[] = []; - private _userProcessFn!: (item: any) => Promise | Observable | Subject; - - constructor( - private readonly angularUtilService: AngularUtilService, - private readonly appRef: ApplicationRef, - private readonly eventPubSubService: EventPubSubService, - private readonly sharedService: SharedService, - private rxjs?: RxJsFacade, - ) { - this._eventHandler = new Slick.EventHandler(); - } - - private get datasetIdPropName(): string { - return this.gridOptions.datasetIdPropertyName || 'id'; - } - - get eventHandler(): SlickEventHandler { - return this._eventHandler; - } - - get gridOptions(): GridOption { - return (this.sharedService?.gridOptions ?? {}) as GridOption; - } - - get rowDetailViewOptions(): RowDetailView | undefined { - return this.gridOptions.rowDetailView; - } - - addRxJsResource(rxjs: RxJsFacade) { - this.rxjs = rxjs; - } - - /** Dispose of the RowDetailView Extension */ - dispose() { - // unsubscribe all SlickGrid events - this._eventHandler.unsubscribeAll(); - - if (this._addon && this._addon.destroy) { - this._addon.destroy(); - } - this._addonOptions = null; - - // also unsubscribe all RxJS subscriptions - this._subscriptions = unsubscribeAllObservables(this._subscriptions); - this.disposeAllViewComponents(); - } - - /** Dispose of all the opened Row Detail Panels Angular View Components */ - disposeAllViewComponents() { - this._views.forEach((compRef) => this.disposeViewComponent(compRef)); - this._views = []; - } - - /** - * Create the plugin before the Grid creation, else it will behave oddly. - * Mostly because the column definitions might change after the grid creation - */ - create(columnDefinitions: Column[], gridOptions: GridOption): SlickRowDetailView | null { - if (columnDefinitions && gridOptions) { - if (!gridOptions.rowDetailView) { - throw new Error('The Row Detail View requires options to be passed via the "rowDetailView" property of the Grid Options'); - } - - if (gridOptions?.rowDetailView) { - if (!this._addon) { - if (typeof gridOptions.rowDetailView.process === 'function') { - // we need to keep the user "process" method and replace it with our own execution method - // we do this because when we get the item detail, we need to call "onAsyncResponse.notify" for the plugin to work - this._userProcessFn = gridOptions.rowDetailView.process as (item: any) => Observable; // keep user's process method - gridOptions.rowDetailView.process = (item) => this.onProcessing(item); // replace process method & run our internal one - } else { - throw new Error('You need to provide a "process" function for the Row Detail Extension to work properly'); - } - - // load the Preload & RowDetail Templates (could be straight HTML or Angular View/ViewModel) - // when those are Angular View/ViewModel, we need to create View Component & provide the html containers to the Plugin (preTemplate/postTemplate methods) - if (!gridOptions.rowDetailView.preTemplate) { - this._preloadComponent = gridOptions?.rowDetailView?.preloadComponent; - gridOptions.rowDetailView.preTemplate = () => DOMPurify.sanitize(`
      `); - } - if (!gridOptions.rowDetailView.postTemplate) { - this._viewComponent = gridOptions?.rowDetailView?.viewComponent; - gridOptions.rowDetailView.postTemplate = (itemDetail: any) => DOMPurify.sanitize(`
      `); - } - - // finally register the Row Detail View Plugin - this._addonOptions = gridOptions.rowDetailView; - this._addon = new Slick.Plugins.RowDetailView(this._addonOptions); - } - const iconColumn: Column = this._addon.getColumnDefinition(); - if (typeof iconColumn === 'object') { - iconColumn.excludeFromExport = true; - iconColumn.excludeFromColumnPicker = true; - iconColumn.excludeFromGridMenu = true; - iconColumn.excludeFromQuery = true; - iconColumn.excludeFromHeaderMenu = true; - - // column index position in the grid - const columnPosition = gridOptions && gridOptions.rowDetailView && gridOptions.rowDetailView.columnIndexPosition || 0; - if (columnPosition > 0) { - columnDefinitions.splice(columnPosition, 0, iconColumn); - } else { - columnDefinitions.unshift(iconColumn); - } - } - } - return this._addon; - } - return null; - } - - /** Get the instance of the SlickGrid addon (control or plugin). */ - getAddonInstance() { - return this._addon; - } - - register(rowSelectionPlugin?: any) { - if (this.sharedService?.slickGrid && this.sharedService.gridOptions) { - // the plugin has to be created BEFORE the grid (else it behaves oddly), but we can only watch grid events AFTER the grid is created - this.sharedService.slickGrid.registerPlugin(this._addon); - - // this also requires the Row Selection Model to be registered as well - if (!rowSelectionPlugin || !this.sharedService.slickGrid.getSelectionModel()) { - rowSelectionPlugin = new Slick.RowSelectionModel(this.sharedService.gridOptions.rowSelectionOptions || { selectActiveRow: true }); - this.sharedService.slickGrid.setSelectionModel(rowSelectionPlugin); - } - - // hook all events - if (this.sharedService.slickGrid && this.rowDetailViewOptions) { - if (this.rowDetailViewOptions.onExtensionRegistered) { - this.rowDetailViewOptions.onExtensionRegistered(this._addon); - } - this._eventHandler.subscribe(this._addon.onAsyncResponse, (e: any, args: { item: any; detailView: RowDetailView }) => { - if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onAsyncResponse === 'function') { - this.rowDetailViewOptions.onAsyncResponse(e, args); - } - }); - this._eventHandler.subscribe(this._addon.onAsyncEndUpdate, (e: any, args: { grid: SlickGrid; item: any; }) => { - // triggers after backend called "onAsyncResponse.notify()" - this.renderViewModel(args && args.item); - - if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onAsyncEndUpdate === 'function') { - this.rowDetailViewOptions.onAsyncEndUpdate(e, args); - } - }); - this._eventHandler.subscribe(this._addon.onAfterRowDetailToggle, (e: any, args: { grid: SlickGrid; item: any; expandedRows: number[]; }) => { - // display preload template & re-render all the other Detail Views after toggling - // the preload View will eventually go away once the data gets loaded after the "onAsyncEndUpdate" event - this.renderPreloadView(); - this.renderAllViewComponents(); - - if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onAfterRowDetailToggle === 'function') { - this.rowDetailViewOptions.onAfterRowDetailToggle(e, args); - } - }); - this._eventHandler.subscribe(this._addon.onBeforeRowDetailToggle, (e: any, args: { grid: SlickGrid; item: any; }) => { - // before toggling row detail, we need to create View Component if it doesn't exist - this.onBeforeRowDetailToggle(e, args); - - if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onBeforeRowDetailToggle === 'function') { - this.rowDetailViewOptions.onBeforeRowDetailToggle(e, args); - } - }); - this._eventHandler.subscribe(this._addon.onRowBackToViewportRange, (e: any, args: { grid: SlickGrid; item: any; rowId: number; rowIndex: number; expandedRows: any[]; rowIdsOutOfViewport: number[]; }) => { - // when row is back to viewport range, we will re-render the View Component(s) - this.onRowBackToViewportRange(e, args); - - if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onRowBackToViewportRange === 'function') { - this.rowDetailViewOptions.onRowBackToViewportRange(e, args); - } - }); - this._eventHandler.subscribe(this._addon.onRowOutOfViewportRange, (e: any, args: { grid: SlickGrid; item: any; rowId: number; rowIndex: number; expandedRows: any[]; rowIdsOutOfViewport: number[]; }) => { - if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onRowOutOfViewportRange === 'function') { - this.rowDetailViewOptions.onRowOutOfViewportRange(e, args); - } - }); - - // -- - // hook some events needed by the Plugin itself - - // we need to redraw the open detail views if we change column position (column reorder) - this._eventHandler.subscribe(this.sharedService.slickGrid.onColumnsReordered, this.redrawAllViewComponents.bind(this)); - - // on row selection changed, we also need to redraw - if (this.gridOptions.enableRowSelection || this.gridOptions.enableCheckboxSelector) { - this._eventHandler.subscribe(this.sharedService.slickGrid.onSelectedRowsChanged, this.redrawAllViewComponents.bind(this)); - } - - // on sort, all row detail are collapsed so we can dispose of all the Views as well - this._eventHandler.subscribe(this.sharedService.slickGrid.onSort, this.disposeAllViewComponents.bind(this)); - - // on filter changed, we need to re-render all Views - this._subscriptions.push( - this.eventPubSubService.subscribe('onFilterChanged', this.redrawAllViewComponents.bind(this)) - ); - } - return this._addon; - } - return null; - } - - /** Redraw (re-render) all the expanded row detail View Components */ - redrawAllViewComponents() { - this._views.forEach((compRef) => { - this.redrawViewComponent(compRef); - }); - } - - /** Render all the expanded row detail View Components */ - renderAllViewComponents() { - this._views.forEach((view) => { - if (view && view.dataContext) { - this.renderViewModel(view.dataContext); - } - }); - } - - /** Redraw the necessary View Component */ - redrawViewComponent(createdView: CreatedView) { - const containerElements = document.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${createdView.id}`); - if (containerElements && containerElements.length >= 0) { - this.renderViewModel(createdView.dataContext); - } - } - - /** Render (or re-render) the View Component (Row Detail) */ - renderPreloadView() { - const containerElements = document.getElementsByClassName(`${PRELOAD_CONTAINER_PREFIX}`); - if (containerElements && containerElements.length >= 0) { - this.angularUtilService.createAngularComponentAppendToDom(this._preloadComponent, containerElements[containerElements.length - 1], true); - } - } - - /** Render (or re-render) the View Component (Row Detail) */ - renderViewModel(item: any): CreatedView | undefined { - const containerElements = document.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${item[this.datasetIdPropName]}`); - if (containerElements && containerElements.length > 0) { - const componentOutput = this.angularUtilService.createAngularComponentAppendToDom(this._viewComponent, containerElements[containerElements.length - 1], true); - if (componentOutput && componentOutput.componentRef && componentOutput.componentRef.instance) { - // pass a few properties to the Row Detail template component - Object.assign(componentOutput.componentRef.instance, { - model: item, - addon: this._addon, - grid: this.sharedService.slickGrid, - dataView: this.sharedService.dataView, - parent: this.rowDetailViewOptions && this.rowDetailViewOptions.parent, - }); - - const viewObj = this._views.find(obj => obj.id === item[this.datasetIdPropName]); - if (viewObj) { - viewObj.componentRef = componentOutput.componentRef; - } - return viewObj; - } - } - return undefined; - } - - // -- - // private functions - // ------------------ - - private disposeViewComponent(expandedView: CreatedView) { - const compRef = expandedView?.componentRef; - if (compRef) { - this.appRef.detachView(compRef.hostView); - if (compRef?.destroy) { - compRef.destroy(); - } - return expandedView; - } - return null; - } - - /** - * notify the onAsyncResponse with the "args.item" (required property) - * the plugin will then use item to populate the row detail panel with the "postTemplate" - * @param item - */ - private notifyTemplate(item: any) { - if (this._addon) { - this._addon.onAsyncResponse.notify({ item }, undefined, this); - } - } - - /** - * On Processing, we will notify the plugin with the new item detail once backend server call completes - * @param item - */ - private async onProcessing(item: any) { - if (item && typeof this._userProcessFn === 'function') { - let awaitedItemDetail: any; - const userProcessFn = this._userProcessFn(item); - - // wait for the "userProcessFn", once resolved we will save it into the "collection" - const response: any | any[] = await userProcessFn; - - if (response.hasOwnProperty(this.datasetIdPropName)) { - awaitedItemDetail = response; // from Promise - } else if (response && response instanceof Observable || response instanceof Promise) { - awaitedItemDetail = await castObservableToPromise(this.rxjs as RxJsFacade, response); // from Angular-http-client - } - - if (!awaitedItemDetail || !awaitedItemDetail.hasOwnProperty(this.datasetIdPropName)) { - throw new Error(`[Angular-Slickgrid] could not process the Row Detail, you must make sure that your "process" callback - (a Promise or an HttpClient call returning an Observable) returns an item object that has an "${this.datasetIdPropName}" property`); - } - - // notify the plugin with the new item details - this.notifyTemplate(awaitedItemDetail || {}); - } - } - - /** - * Just before the row get expanded or collapsed we will do the following - * First determine if the row is expanding or collapsing, - * if it's expanding we will add it to our View Components reference array if we don't already have it - * or if it's collapsing we will remove it from our View Components reference array - */ - private onBeforeRowDetailToggle(e: Event, args: { grid: SlickGrid; item: any; }) { - // expanding - if (args && args.item && args.item.__collapsed) { - // expanding row detail - const viewInfo: CreatedView = { - id: args.item[this.datasetIdPropName], - dataContext: args.item - }; - const idPropName = this.gridOptions.datasetIdPropertyName || 'id'; - addToArrayWhenNotExists(this._views, viewInfo, idPropName); - } else { - // collapsing, so dispose of the View/Component - const foundViewIndex = this._views.findIndex((view: CreatedView) => view.id === args.item[this.datasetIdPropName]); - if (foundViewIndex >= 0 && this._views.hasOwnProperty(foundViewIndex)) { - const compRef = this._views[foundViewIndex].componentRef; - if (compRef) { - this.appRef.detachView(compRef.hostView); - compRef.destroy(); - } - this._views.splice(foundViewIndex, 1); - } - } - } - - /** When Row comes back to Viewport Range, we need to redraw the View */ - private onRowBackToViewportRange(e: Event, args: { grid: SlickGrid; item: any; rowId: number; rowIndex: number; expandedRows: any[]; rowIdsOutOfViewport: number[]; }) { - if (args && args.item) { - this._views.forEach((view) => { - if (view.id === args.item[this.datasetIdPropName]) { - this.redrawViewComponent(view); - } - }); - } - } -} diff --git a/src/app/modules/angular-slickgrid/extensions/slickRowDetailView.ts b/src/app/modules/angular-slickgrid/extensions/slickRowDetailView.ts new file mode 100644 index 000000000..6378ac493 --- /dev/null +++ b/src/app/modules/angular-slickgrid/extensions/slickRowDetailView.ts @@ -0,0 +1,377 @@ +import 'slickgrid/plugins/slick.rowdetailview'; +import 'slickgrid/plugins/slick.rowselectionmodel'; + +import { ApplicationRef, ComponentRef, Injectable, Type, ViewContainerRef } from '@angular/core'; +import { + addToArrayWhenNotExists, + castObservableToPromise, + RxJsFacade, + SlickEventHandler, + SlickGrid, + SlickRowSelectionModel, +} from '@slickgrid-universal/common'; +import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; +import { SlickRowDetailView as UniversalSlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin'; +import { Observable, Subject, Subscription } from 'rxjs'; +import * as DOMPurify_ from 'dompurify'; +const DOMPurify = DOMPurify_; // patch to fix rollup to work + +import { GridOption, RowDetailView } from '../models/index'; +import { AngularUtilService } from '../services/angularUtil.service'; +import { unsubscribeAllObservables } from '../services/utilities'; + +const ROW_DETAIL_CONTAINER_PREFIX = 'container_'; +const PRELOAD_CONTAINER_PREFIX = 'container_loading'; + +export interface CreatedView { + id: string | number; + dataContext: any; + componentRef?: ComponentRef; +} + +@Injectable() +export class SlickRowDetailView extends UniversalSlickRowDetailView { + rowDetailContainer!: ViewContainerRef; + protected _eventHandler!: SlickEventHandler; + protected _preloadComponent: Type | undefined; + protected _views: CreatedView[] = []; + protected _viewComponent!: Type; + protected _subscriptions: Subscription[] = []; + protected _userProcessFn!: (item: any) => Promise | Observable | Subject; + + constructor( + protected readonly angularUtilService: AngularUtilService, + protected readonly appRef: ApplicationRef, + protected readonly eventPubSubService: EventPubSubService, + protected rxjs?: RxJsFacade, + ) { + super(); + } + + get addonOptions() { + return this.getOptions(); + } + + protected get datasetIdPropName(): string { + return this.gridOptions.datasetIdPropertyName || 'id'; + } + + get eventHandler(): SlickEventHandler { + return this._eventHandler; + } + set eventHandler(eventHandler: SlickEventHandler) { + this._eventHandler = eventHandler; + } + + /** Getter for the Grid Options pulled through the Grid Object */ + get gridOptions(): GridOption { + return (this._grid?.getOptions() || {}) as GridOption; + } + + get rowDetailViewOptions(): RowDetailView | undefined { + return this.gridOptions.rowDetailView; + } + + addRxJsResource(rxjs: RxJsFacade) { + this.rxjs = rxjs; + } + + /** Dispose of the RowDetailView Extension */ + dispose() { + this.disposeAllViewComponents(); + this._subscriptions = unsubscribeAllObservables(this._subscriptions); // also unsubscribe all RxJS subscriptions + super.dispose(); + } + + /** Dispose of all the opened Row Detail Panels Angular View Components */ + disposeAllViewComponents() { + this._views.forEach((compRef) => this.disposeViewComponent(compRef)); + this._views = []; + } + + /** Get the instance of the SlickGrid addon (control or plugin). */ + getAddonInstance(): SlickRowDetailView | null { + return this; + } + + init(grid: SlickGrid) { + this._grid = grid; + super.init(this._grid); + this.register(grid?.getSelectionModel()); + } + + /** + * Create the plugin before the Grid creation, else it will behave oddly. + * Mostly because the column definitions might change after the grid creation + */ + register(rowSelectionPlugin?: SlickRowSelectionModel) { + if (typeof this.gridOptions.rowDetailView?.process === 'function') { + // we need to keep the user "process" method and replace it with our own execution method + // we do this because when we get the item detail, we need to call "onAsyncResponse.notify" for the plugin to work + this._userProcessFn = this.gridOptions.rowDetailView.process as (item: any) => Promise; // keep user's process method + this.gridOptions.rowDetailView.process = (item) => this.onProcessing(item); // replace process method & run our internal one + } else { + throw new Error('[Angular-Slickgrid] You need to provide a "process" function for the Row Detail Extension to work properly'); + } + + if (this._grid && this.gridOptions?.rowDetailView) { + // load the Preload & RowDetail Templates (could be straight HTML or Angular View/ViewModel) + // when those are Angular View/ViewModel, we need to create View Component & provide the html containers to the Plugin (preTemplate/postTemplate methods) + if (!this.gridOptions.rowDetailView.preTemplate) { + this._preloadComponent = this.gridOptions?.rowDetailView?.preloadComponent; + this.gridOptions.rowDetailView.preTemplate = () => DOMPurify.sanitize(`
      `); + } + if (!this.gridOptions.rowDetailView.postTemplate) { + this._viewComponent = this.gridOptions?.rowDetailView?.viewComponent; + this.gridOptions.rowDetailView.postTemplate = (itemDetail: any) => DOMPurify.sanitize(`
      `); + } + + // this also requires the Row Selection Model to be registered as well + if (!rowSelectionPlugin || !this._grid.getSelectionModel()) { + rowSelectionPlugin = new SlickRowSelectionModel(this.gridOptions.rowSelectionOptions || { selectActiveRow: true }); + this._grid.setSelectionModel(rowSelectionPlugin); + } + + // hook all events + if (this._grid && this.rowDetailViewOptions) { + if (this.rowDetailViewOptions.onExtensionRegistered) { + this.rowDetailViewOptions.onExtensionRegistered(this); + } + + if (this.onAsyncResponse) { + this._eventHandler.subscribe(this.onAsyncResponse, (event, args) => { + if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onAsyncResponse === 'function') { + this.rowDetailViewOptions.onAsyncResponse(event, args); + } + }); + } + + if (this.onAsyncEndUpdate) { + this._eventHandler.subscribe(this.onAsyncEndUpdate, (e: any, args: { grid: SlickGrid; item: any; }) => { + // triggers after backend called "onAsyncResponse.notify()" + this.renderViewModel(args?.item); + + if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onAsyncEndUpdate === 'function') { + this.rowDetailViewOptions.onAsyncEndUpdate(e, args); + } + }); + } + + if (this.onAfterRowDetailToggle) { + this._eventHandler.subscribe(this.onAfterRowDetailToggle, (e: any, args: { grid: SlickGrid; item: any; expandedRows: number[]; }) => { + // display preload template & re-render all the other Detail Views after toggling + // the preload View will eventually go away once the data gets loaded after the "onAsyncEndUpdate" event + this.renderPreloadView(); + this.renderAllViewComponents(); + + if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onAfterRowDetailToggle === 'function') { + this.rowDetailViewOptions.onAfterRowDetailToggle(e, args); + } + }); + } + + if (this.onBeforeRowDetailToggle) { + this._eventHandler.subscribe(this.onBeforeRowDetailToggle, (e, args) => { + // before toggling row detail, we need to create View Component if it doesn't exist + this.handleOnBeforeRowDetailToggle(e, args); + + if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onBeforeRowDetailToggle === 'function') { + this.rowDetailViewOptions.onBeforeRowDetailToggle(e, args); + } + }); + } + + if (this.onRowBackToViewportRange) { + this._eventHandler.subscribe(this.onRowBackToViewportRange, (e: any, args: { grid: SlickGrid; item: any; rowId: number; rowIndex: number; expandedRows: any[]; rowIdsOutOfViewport: number[]; }) => { + // when row is back to viewport range, we will re-render the View Component(s) + this.handleOnRowBackToViewportRange(e, args); + + if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onRowBackToViewportRange === 'function') { + this.rowDetailViewOptions.onRowBackToViewportRange(e, args); + } + }); + } + + if (this.onRowOutOfViewportRange) { + this._eventHandler.subscribe(this.onRowOutOfViewportRange, (e: any, args: { grid: SlickGrid; item: any; rowId: number; rowIndex: number; expandedRows: any[]; rowIdsOutOfViewport: number[]; }) => { + if (this.rowDetailViewOptions && typeof this.rowDetailViewOptions.onRowOutOfViewportRange === 'function') { + this.rowDetailViewOptions.onRowOutOfViewportRange(e, args); + } + }); + } + + // -- + // hook some events needed by the Plugin itself + + // we need to redraw the open detail views if we change column position (column reorder) + this._eventHandler.subscribe(this._grid.onColumnsReordered, this.redrawAllViewComponents.bind(this)); + + // on row selection changed, we also need to redraw + if (this.gridOptions.enableRowSelection || this.gridOptions.enableCheckboxSelector) { + this._eventHandler.subscribe(this._grid.onSelectedRowsChanged, this.redrawAllViewComponents.bind(this)); + } + + // on sort, all row detail are collapsed so we can dispose of all the Views as well + this._eventHandler.subscribe(this._grid.onSort, this.disposeAllViewComponents.bind(this)); + + // on filter changed, we need to re-render all Views + this._subscriptions.push( + this.eventPubSubService.subscribe('onFilterChanged', this.redrawAllViewComponents.bind(this)) + ); + } + } + return this; + } + + /** Redraw (re-render) all the expanded row detail View Components */ + redrawAllViewComponents() { + this._views.forEach((compRef) => { + this.redrawViewComponent(compRef); + }); + } + + /** Render all the expanded row detail View Components */ + renderAllViewComponents() { + this._views.forEach((view) => { + if (view && view.dataContext) { + this.renderViewModel(view.dataContext); + } + }); + } + + /** Redraw the necessary View Component */ + redrawViewComponent(createdView: CreatedView) { + const containerElements = document.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${createdView.id}`); + if (containerElements && containerElements.length >= 0) { + this.renderViewModel(createdView.dataContext); + } + } + + /** Render (or re-render) the View Component (Row Detail) */ + renderPreloadView() { + const containerElements = document.getElementsByClassName(`${PRELOAD_CONTAINER_PREFIX}`); + if (containerElements && containerElements.length >= 0) { + this.angularUtilService.createAngularComponentAppendToDom(this._preloadComponent, containerElements[containerElements.length - 1], true); + } + } + + /** Render (or re-render) the View Component (Row Detail) */ + renderViewModel(item: any): CreatedView | undefined { + const containerElements = document.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${item[this.datasetIdPropName]}`); + if (containerElements && containerElements.length > 0) { + const componentOutput = this.angularUtilService.createAngularComponentAppendToDom(this._viewComponent, containerElements[containerElements.length - 1], true); + if (componentOutput && componentOutput.componentRef && componentOutput.componentRef.instance) { + // pass a few properties to the Row Detail template component + Object.assign(componentOutput.componentRef.instance, { + model: item, + addon: this, + grid: this._grid, + dataView: this.dataView, + parent: this.rowDetailViewOptions && this.rowDetailViewOptions.parent, + }); + + const viewObj = this._views.find(obj => obj.id === item[this.datasetIdPropName]); + if (viewObj) { + viewObj.componentRef = componentOutput.componentRef; + } + return viewObj; + } + } + return undefined; + } + + // -- + // protected functions + // ------------------ + + protected disposeViewComponent(expandedView: CreatedView): CreatedView | void { + const compRef = expandedView?.componentRef; + if (compRef) { + this.appRef.detachView(compRef.hostView); + if (compRef?.destroy) { + compRef.destroy(); + } + return expandedView; + } + } + + /** + * notify the onAsyncResponse with the "args.item" (required property) + * the plugin will then use item to populate the row detail panel with the "postTemplate" + * @param item + */ + protected notifyTemplate(item: any) { + if (this.onAsyncResponse) { + this.onAsyncResponse.notify({ item }, undefined, this); + } + } + + /** + * On Processing, we will notify the plugin with the new item detail once backend server call completes + * @param item + */ + protected async onProcessing(item: any) { + if (item && typeof this._userProcessFn === 'function') { + let awaitedItemDetail: any; + const userProcessFn = this._userProcessFn(item); + + // wait for the "userProcessFn", once resolved we will save it into the "collection" + const response: any | any[] = await userProcessFn; + + if (response.hasOwnProperty(this.datasetIdPropName)) { + awaitedItemDetail = response; // from Promise + } else if (response && response instanceof Observable || response instanceof Promise) { + awaitedItemDetail = await castObservableToPromise(this.rxjs as RxJsFacade, response); // from Angular-http-client + } + + if (!awaitedItemDetail || !awaitedItemDetail.hasOwnProperty(this.datasetIdPropName)) { + throw new Error(`[Angular-Slickgrid] could not process the Row Detail, you must make sure that your "process" callback + (a Promise or an HttpClient call returning an Observable) returns an item object that has an "${this.datasetIdPropName}" property`); + } + + // notify the plugin with the new item details + this.notifyTemplate(awaitedItemDetail || {}); + } + } + + /** + * Just before the row get expanded or collapsed we will do the following + * First determine if the row is expanding or collapsing, + * if it's expanding we will add it to our View Components reference array if we don't already have it + * or if it's collapsing we will remove it from our View Components reference array + */ + protected handleOnBeforeRowDetailToggle(e: Event, args: { grid: SlickGrid; item: any; }) { + // expanding + if (args && args.item && args.item.__collapsed) { + // expanding row detail + const viewInfo: CreatedView = { + id: args.item[this.datasetIdPropName], + dataContext: args.item + }; + const idPropName = this.gridOptions.datasetIdPropertyName || 'id'; + addToArrayWhenNotExists(this._views, viewInfo, idPropName); + } else { + // collapsing, so dispose of the View/Component + const foundViewIndex = this._views.findIndex((view: CreatedView) => view.id === args.item[this.datasetIdPropName]); + if (foundViewIndex >= 0 && this._views.hasOwnProperty(foundViewIndex)) { + const compRef = this._views[foundViewIndex].componentRef; + if (compRef) { + this.appRef.detachView(compRef.hostView); + compRef.destroy(); + } + this._views.splice(foundViewIndex, 1); + } + } + } + + /** When Row comes back to Viewport Range, we need to redraw the View */ + protected handleOnRowBackToViewportRange(e: Event, args: { grid: SlickGrid; item: any; rowId: number; rowIndex: number; expandedRows: any[]; rowIdsOutOfViewport: number[]; }) { + if (args && args.item) { + this._views.forEach((view) => { + if (view.id === args.item[this.datasetIdPropName]) { + this.redrawViewComponent(view); + } + }); + } + } +} diff --git a/src/app/modules/angular-slickgrid/global-grid-options.ts b/src/app/modules/angular-slickgrid/global-grid-options.ts index 76ba47600..4f8669a0b 100644 --- a/src/app/modules/angular-slickgrid/global-grid-options.ts +++ b/src/app/modules/angular-slickgrid/global-grid-options.ts @@ -135,6 +135,7 @@ export const GlobalGridOptions: Partial = { forceFitColumns: false, frozenHeaderWidthCalcDifferential: 0, gridMenu: { + dropSide: 'left', commandLabels: { clearAllFiltersCommandKey: 'CLEAR_ALL_FILTERS', clearAllSortingCommandKey: 'CLEAR_ALL_SORTING', @@ -169,7 +170,6 @@ export const GlobalGridOptions: Partial = { iconTogglePreHeaderCommand: 'fa fa-random', menuWidth: 16, resizeOnShowHeaderRow: true, - useClickToRepositionMenu: false, // use icon location to reposition instead headerColumnValueExtractor: pickerHeaderColumnValueExtractor }, headerMenu: { @@ -203,6 +203,7 @@ export const GlobalGridOptions: Partial = { }, // technically speaking the Row Detail requires the process & viewComponent but we'll ignore it just to set certain options rowDetailView: { + collapseAllOnSort: true, cssClass: 'detail-view-toggle', panelRows: 1, keyPrefix: '__', diff --git a/src/app/modules/angular-slickgrid/index.spec.ts b/src/app/modules/angular-slickgrid/index.spec.ts index e20e36a19..abef1facb 100644 --- a/src/app/modules/angular-slickgrid/index.spec.ts +++ b/src/app/modules/angular-slickgrid/index.spec.ts @@ -9,7 +9,7 @@ describe('Testing library entry point', () => { expect(typeof entry.AngularSlickgridComponent).toBe('function'); expect(typeof entry.AngularSlickgridModule).toBe('function'); expect(typeof entry.SlickgridConfig).toBe('function'); - expect(typeof entry.RowDetailViewExtension).toBe('function'); + expect(typeof entry.SlickRowDetailView).toBe('function'); expect(typeof entry.AngularUtilService).toBe('function'); expect(typeof entry.BsDropDownService).toBe('function'); expect(typeof entry.unsubscribeAllObservables).toBe('function'); diff --git a/src/app/modules/angular-slickgrid/index.ts b/src/app/modules/angular-slickgrid/index.ts index acd1ac6af..351fa2a5e 100644 --- a/src/app/modules/angular-slickgrid/index.ts +++ b/src/app/modules/angular-slickgrid/index.ts @@ -3,7 +3,7 @@ export * from '@slickgrid-universal/common'; // Public classes. export { AngularComponentOutput, AngularGridInstance, GridOption, SlickGrid, RowDetailView } from './models/index'; export { AngularUtilService, BsDropDownService, unsubscribeAllObservables } from './services/index'; -export { RowDetailViewExtension } from './extensions/index'; +export { SlickRowDetailView } from './extensions/index'; // components & module export { SlickgridConfig } from './slickgrid-config'; diff --git a/test/cypress/integration/example01.spec.js b/test/cypress/integration/example01.spec.js index 148c4f388..d511a2892 100644 --- a/test/cypress/integration/example01.spec.js +++ b/test/cypress/integration/example01.spec.js @@ -34,15 +34,15 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { .find('.slick-header-column') .first() .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(4)') + .children('.slick-header-menu-content') .should('contain', 'Sort Descending') .click(); @@ -60,14 +60,14 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { .find('.slick-header-column') .first() .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(3)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(3)') + .children('.slick-header-menu-content') .should('contain', 'Sort Ascending') .click(); @@ -87,14 +87,14 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { cy.get('#grid2') .find('.slick-header-column:nth-child(2)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .invoke('show') .click(); cy.get('#grid2') .find('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') + .children('.slick-header-menu-item:nth-child(4)') .click(); cy.get('#grid2') @@ -112,7 +112,7 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { it('should clear sorting of grid2 using the Grid Menu "Clear all Sorting" command', () => { cy.get('#grid2') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); }); @@ -128,8 +128,8 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { expect(gridUid).to.not.be.null; }) .then(() => { - cy.get(`.slick-gridmenu.${gridUid}`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu.${gridUid}`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Sorting') @@ -202,7 +202,7 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { it('should open the Grid Menu on 1st Grid and expect all Columns to be checked', () => { let gridUid = ''; cy.get('#grid1') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click({ force: true }); cy.get('#grid1') @@ -212,14 +212,14 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { expect(gridUid).to.not.be.null; }) .then(() => { - cy.get(`.slick-gridmenu.${gridUid}`) - .find('.slick-gridmenu-list') + cy.get(`.slick-grid-menu.${gridUid}`) + .find('.slick-grid-menu-list') .children('li') .each(($child, index) => { if (index <= 5) { const $input = $child.children('input'); const $label = $child.children('label'); - expect($input.attr('checked')).to.eq('checked'); + expect($input.prop('checked')).to.eq(true); expect($label.text()).to.eq(fullTitles[index]); } }); @@ -229,15 +229,15 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { it('should then hide "Title" column from same 1st Grid and expect the column to be removed from 1st Grid', () => { const newColumnList = ['Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; cy.get('#grid1') - .get('.slick-gridmenu:visible') - .find('.slick-gridmenu-list') + .get('.slick-grid-menu:visible') + .find('.slick-grid-menu-list') .children('li:visible:nth(0)') .children('label') .should('contain', 'Title') .click({ force: true }); cy.get('#grid1') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .click({ force: true }); @@ -250,7 +250,7 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { it('should open the Grid Menu off 2nd Grid and expect all Columns to still be all checked', () => { let gridUid = ''; cy.get('#grid2') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click({ force: true }); cy.get('#grid2') @@ -260,14 +260,14 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { expect(gridUid).to.not.be.null; }) .then(() => { - cy.get(`.slick-gridmenu.${gridUid}`) - .find('.slick-gridmenu-list') + cy.get(`.slick-grid-menu.${gridUid}`) + .find('.slick-grid-menu-list') .children('li') .each(($child, index) => { if (index <= 5) { const $input = $child.children('input'); const $label = $child.children('label'); - expect($input.attr('checked')).to.eq('checked'); + expect($input.prop('checked')).to.eq(true); expect($label.text()).to.eq(fullTitles[index]); } }); @@ -277,15 +277,15 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { it('should then hide "% Complete" column from this same 2nd Grid and expect the column to be removed from 2nd Grid', () => { const newColumnList = ['Title', 'Duration (days)', 'Start', 'Finish', 'Effort Driven']; cy.get('#grid2') - .get('.slick-gridmenu:visible') - .find('.slick-gridmenu-list') + .get('.slick-grid-menu:visible') + .find('.slick-grid-menu-list') .children('li:visible:nth(2)') .children('label') .should('contain', '% Complete') .click({ force: true }); cy.get('#grid2') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .click({ force: true }); @@ -297,10 +297,10 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { it('should go back to 1st Grid and open its Grid Menu and we expect this grid to stil have the "Title" column be hidden (unchecked)', () => { cy.get('#grid1') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click({ force: true }); - cy.get('.slick-gridmenu-list') + cy.get('.slick-grid-menu-list') .children('li') .each(($child, index) => { if (index <= 5) { @@ -309,7 +309,7 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { if ($label.text() === 'Title') { expect($input.attr('checked')).to.eq(undefined); } else { - expect($input.attr('checked')).to.eq('checked'); + expect($input.prop('checked')).to.eq(true); } expect($label.text()).to.eq(fullTitles[index]); } @@ -319,15 +319,15 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { it('should hide "Start" column from 1st Grid and expect to have 2 hidden columns (Title, Start)', () => { const newColumnList = ['Duration (days)', '% Complete', 'Finish', 'Effort Driven']; cy.get('#grid1') - .get('.slick-gridmenu:visible') - .find('.slick-gridmenu-list') + .get('.slick-grid-menu:visible') + .find('.slick-grid-menu-list') .children('li:visible:nth(3)') .children('label') .should('contain', 'Start') .click({ force: true }); cy.get('#grid1') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .click({ force: true }); @@ -380,7 +380,7 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { it('should open the Grid Menu on 2nd Grid and expect all Columns to be checked', () => { let gridUid = ''; cy.get('#grid2') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click({ force: true }); cy.get('#grid2') @@ -390,14 +390,14 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { expect(gridUid).to.not.be.null; }) .then(() => { - cy.get(`.slick-gridmenu.${gridUid}`) - .find('.slick-gridmenu-list') + cy.get(`.slick-grid-menu.${gridUid}`) + .find('.slick-grid-menu-list') .children('li') .each(($child, index) => { if (index <= 5) { const $input = $child.children('input'); const $label = $child.children('label'); - expect($input.attr('checked')).to.eq('checked'); + expect($input.prop('checked')).to.eq(true); expect($label.text()).to.eq(fullTitles[index]); } }); @@ -416,7 +416,7 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { it('should open the Grid Menu on 1st Grid and also expect to only have 4 columns checked (visible)', () => { let gridUid = ''; cy.get('#grid1') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click({ force: true }); cy.get('#grid1') @@ -426,8 +426,8 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { expect(gridUid).to.not.be.null; }) .then(() => { - cy.get(`.slick-gridmenu.${gridUid}`) - .find('.slick-gridmenu-list') + cy.get(`.slick-grid-menu.${gridUid}`) + .find('.slick-grid-menu-list') .children('li') .each(($child, index) => { if (index <= 5) { @@ -436,7 +436,7 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { if ($label.text() === 'Title' || $label.text() === 'Start') { expect($input.attr('checked')).to.eq(undefined); } else { - expect($input.attr('checked')).to.eq('checked'); + expect($input.prop('checked')).to.eq(true); } expect($label.text()).to.eq(fullTitles[index]); } @@ -444,7 +444,7 @@ describe('Example 1 - Basic Grids', { retries: 1 }, () => { }); cy.get('#grid1') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .click({ force: true }); }); diff --git a/test/cypress/integration/example03.spec.js b/test/cypress/integration/example03.spec.js index c4c562adb..029b3848c 100644 --- a/test/cypress/integration/example03.spec.js +++ b/test/cypress/integration/example03.spec.js @@ -151,14 +151,14 @@ describe('Example 3 - Grid with Editors', { retries: 1 }, () => { cy.get('.slick-header-column:nth-child(14)') .first() .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(6)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(6)') + .children('.slick-header-menu-content') .should('contain', 'Remove Filter') .click(); diff --git a/test/cypress/integration/example05.spec.js b/test/cypress/integration/example05.spec.js index 699cace83..e262ced48 100644 --- a/test/cypress/integration/example05.spec.js +++ b/test/cypress/integration/example05.spec.js @@ -198,12 +198,12 @@ describe('Example 5 - OData Grid', () => { it('should Clear all Filters and expect to go back to first page', () => { cy.get('#grid5') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') @@ -243,12 +243,12 @@ describe('Example 5 - OData Grid', () => { it('should Clear all Sorting', () => { cy.get('#grid5') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item:nth(1)') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item:nth(1)') .find('span') .contains('Clear all Sorting') .click(); @@ -335,12 +335,12 @@ describe('Example 5 - OData Grid', () => { describe('when "enableCount" is unchecked (not set)', () => { it('should Clear all Filters, set 20 items per page & uncheck "enableCount"', () => { cy.get('#grid5') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') @@ -417,12 +417,12 @@ describe('Example 5 - OData Grid', () => { it('should Clear all Sorting', () => { cy.get('#grid5') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item:nth(1)') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item:nth(1)') .find('span') .contains('Clear all Sorting') .click(); @@ -693,14 +693,14 @@ describe('Example 5 - OData Grid', () => { cy.get('#grid5') .find('.slick-header-left .slick-header-column:nth(1)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(6)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(6)') + .children('.slick-header-menu-content') .should('contain', 'Remove Filter') .click(); diff --git a/test/cypress/integration/example06.spec.js b/test/cypress/integration/example06.spec.js index 4eb246a18..0ca03c3f4 100644 --- a/test/cypress/integration/example06.spec.js +++ b/test/cypress/integration/example06.spec.js @@ -170,15 +170,15 @@ describe('Example 6 - GraphQL Grid', { retries: 1 }, () => { cy.get('#grid6') .find('.slick-header-left .slick-header-column:nth(0)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(6)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(6)') + .children('.slick-header-menu-content') .should('contain', 'Remove Filter') .click(); @@ -201,14 +201,14 @@ describe('Example 6 - GraphQL Grid', { retries: 1 }, () => { cy.get('#grid6') .find('.slick-header-left .slick-header-column:nth(0)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(6)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(6)') + .children('.slick-header-menu-content') .should('contain', 'Remove Filter') .click(); @@ -231,15 +231,15 @@ describe('Example 6 - GraphQL Grid', { retries: 1 }, () => { cy.get('#grid6') .find('.slick-header-left .slick-header-column:nth(5)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(6)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(6)') + .children('.slick-header-menu-content') .should('contain', 'Remove Filter') .click(); @@ -405,61 +405,61 @@ describe('Example 6 - GraphQL Grid', { retries: 1 }, () => { .find('.slick-header-columns.slick-header-columns-left .slick-header-column') .first() .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(3)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(3)') + .children('.slick-header-menu-content') .should('contain', 'Sort Ascending'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(4)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(4)') + .children('.slick-header-menu-content') .should('contain', 'Sort Descending'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(6)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(6)') + .children('.slick-header-menu-content') .should('contain', 'Remove Filter'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(7)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(7)') + .children('.slick-header-menu-content') .should('contain', 'Remove Sort'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(8)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(8)') + .children('.slick-header-menu-content') .should('contain', 'Hide Column'); }); it('should open the Grid Menu and expect all commands be displayed in English', () => { cy.get('#grid6') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click'); - cy.get('.slick-gridmenu .title:nth(0)') + cy.get('.slick-grid-menu .title:nth(0)') .contains('Commands'); - cy.get('.slick-gridmenu-item:nth(0) > span') + cy.get('.slick-grid-menu-item:nth(0) > span') .contains('Clear all Filters'); - cy.get('.slick-gridmenu-item:nth(1) > span') + cy.get('.slick-grid-menu-item:nth(1) > span') .contains('Clear all Sorting'); - cy.get('.slick-gridmenu .title:nth(1)') + cy.get('.slick-grid-menu .title:nth(1)') .contains('Columns'); - cy.get('.slick-gridmenu-list li:nth(0)') + cy.get('.slick-grid-menu-list li:nth(0)') .contains('Customer Information - Name'); - cy.get('.slick-gridmenu-list li:nth(1)') + cy.get('.slick-grid-menu-list li:nth(1)') .contains('Customer Information - Gender'); - cy.get('.slick-gridmenu [data-dismiss=slick-gridmenu] > span.close') + cy.get('.slick-grid-menu [data-dismiss=slick-grid-menu] > span.close') .click({ force: true }); }); @@ -513,61 +513,61 @@ describe('Example 6 - GraphQL Grid', { retries: 1 }, () => { .find('.slick-header-columns.slick-header-columns-left .slick-header-column') .first() .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(3)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(3)') + .children('.slick-header-menu-content') .should('contain', 'Trier par ordre croissant'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(4)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(4)') + .children('.slick-header-menu-content') .should('contain', 'Trier par ordre décroissant'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(6)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(6)') + .children('.slick-header-menu-content') .should('contain', 'Supprimer le filtre'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(7)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(7)') + .children('.slick-header-menu-content') .should('contain', 'Supprimer le tri'); cy.get('.slick-header-menu') - .children('.slick-header-menuitem:nth-child(8)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(8)') + .children('.slick-header-menu-content') .should('contain', 'Cacher la colonne'); }); it('should open the Grid Menu and expect all commands be displayed in French', () => { cy.get('#grid6') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click'); - cy.get('.slick-gridmenu .title:nth(0)') + cy.get('.slick-grid-menu .title:nth(0)') .contains('Commandes'); - cy.get('.slick-gridmenu-item:nth(0) > span') + cy.get('.slick-grid-menu-item:nth(0) > span') .contains('Supprimer tous les filtres'); - cy.get('.slick-gridmenu-item:nth(1) > span') + cy.get('.slick-grid-menu-item:nth(1) > span') .contains('Supprimer tous les tris'); - cy.get('.slick-gridmenu .title:nth(1)') + cy.get('.slick-grid-menu .title:nth(1)') .contains('Colonnes'); - cy.get('.slick-gridmenu-list li:nth(0)') + cy.get('.slick-grid-menu-list li:nth(0)') .contains('Information Client - Nom'); - cy.get('.slick-gridmenu-list li:nth(1)') + cy.get('.slick-grid-menu-list li:nth(1)') .contains('Information Client - Sexe'); - cy.get('.slick-gridmenu [data-dismiss=slick-gridmenu] > span.close') + cy.get('.slick-grid-menu [data-dismiss=slick-grid-menu] > span.close') .click({ force: true }); }); diff --git a/test/cypress/integration/example07.spec.js b/test/cypress/integration/example07.spec.js index 751de21f1..3dbbd0f99 100644 --- a/test/cypress/integration/example07.spec.js +++ b/test/cypress/integration/example07.spec.js @@ -10,156 +10,413 @@ describe('Example 7 - Header Button Plugin', { retries: 1 }, () => { }); }); - it('should display Example title', () => { - cy.visit(`${Cypress.config('baseExampleUrl')}/headerbutton`); - cy.get('h2').should('contain', 'Example 7: Header Button Plugin'); - }); + describe('Grid 1', () => { + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseExampleUrl')}/headerbutton`); + cy.get('h2').should('contain', 'Example 7: Header Button Plugin'); + }); - it('should have exact Column Titles in the grid', () => { - cy.get('#grid7') - .find('.slick-header-columns') - .children() - .each(($child, index) => expect($child.text()).to.eq(titles[index])); - }); + it('should have exact Column Titles in the grid', () => { + cy.get('#grid7-1 .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); - it('should go over the 3rd column "Column C" and expect to see negative number in red after clicking on the red header button', () => { - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(2)') - .should('contain', 'Column C'); + it('should go over the 3rd column "Column C" and expect to see negative number in red after clicking on the red header button', () => { + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .should('contain', 'Column C'); - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(2)') - .find('.slick-header-button.fa.fa-circle-o.red.faded') - .click(); + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-header-button.fa.fa-circle-o.red.faded') + .click(); - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(2)') - .find('.slick-header-button.fa.fa-circle-o.red.faded') - .should('not.exist'); // shouldn't be faded anymore + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-header-button.fa.fa-circle-o.red.faded') + .should('not.exist'); // shouldn't be faded anymore - cy.window().then((win) => { - expect(win.console.log).to.have.callCount(1); - expect(win.console.log).to.be.calledWith(`execute a callback action to "toggle-highlight" on Column C`); - }); - - cy.get('#grid7') - .find('.slick-row') - .each(($row, index) => { - if (index > 10) { - return; // check only the first 10 rows is enough - } - cy.wrap($row).children('.slick-cell:nth(2)') - .each($cell => { - const numberValue = $cell.text(); - const htmlValue = $cell.html(); - if (+numberValue < 0) { - expect(htmlValue).to.eq(`
      ${numberValue}
      `); - } else { - expect(htmlValue).to.eq(numberValue); - } - }); + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith(`execute a callback action to "toggle-highlight" on Column C`); }); - }); - it('should go over the 5th column "Column E" and not find the red header button', () => { - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(4)') - .should('contain', 'Column E'); + cy.get('#grid7-1 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; // check only the first 10 rows is enough + } + cy.wrap($row).children('.slick-cell:nth(2)') + .each($cell => { + const numberValue = $cell.text(); + const htmlValue = $cell.html(); + if (+numberValue < 0) { + expect(htmlValue).to.eq(`
      ${numberValue}
      `); + } else { + expect(htmlValue).to.eq(numberValue); + } + }); + }); + }); - // column E should not have the icon - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(4)') - .find('.slick-header-button') - .should('not.exist'); - }); + it('should go over the 5th column "Column E" and not find the red header button', () => { + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(4)') + .should('contain', 'Column E'); + + // column E should not have the icon + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(4)') + .find('.slick-header-button') + .should('not.exist'); + }); + + it('should go over the last "Column J" and expect to find the button to have the disabled class and clicking it should not turn the negative numbers to red neither expect console log after clicking the disabled button', () => { + cy.get('#grid7-1 .slick-viewport-top.slick-viewport-left') + .scrollTo('right') + .wait(50); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .should('contain', 'Column J') + .find('.slick-header-button-disabled') + .should('exist'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .find('.slick-header-button.slick-header-button-disabled.fa.fa-circle-o.red.faded') + .should('exist') + .click(); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .find('.slick-header-button.slick-header-button-disabled.fa.fa-circle-o.red.faded') + .should('exist'); // should still be faded after previous click - it('should go over the last "Column J" and expect to find the red header button, however it should be usable and number should not display as red', () => { - cy.get('.slick-viewport-top.slick-viewport-left') - .scrollTo('right') - .wait(50); - - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(9)') - .should('contain', 'Column J'); - - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(9)') - .find('.slick-header-button.fa.fa-circle-o.red.faded') - .should('exist') - .click(); - - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(9)') - .find('.slick-header-button.fa.fa-circle-o.red.faded') - .should('exist'); // should still be faded - - cy.get('#grid7') - .find('.slick-row') - .each(($row, index) => { - if (index > 10) { - return; - } - cy.wrap($row).children('.slick-cell:nth(9)') - .each($cell => expect($cell.html()).to.eq($cell.text())); + cy.get('#grid7-1 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; + } + cy.wrap($row).children('.slick-cell:nth(9)') + .each($cell => expect($cell.html()).to.eq($cell.text())); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(0); }); + }); + + it('should resize 1st column and make it wider', () => { + cy.get('#grid7-1 .slick-viewport-top.slick-viewport-left') + .scrollTo('left') + .wait(50); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .should('contain', 'Resize me!'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(3)') + .should('be.hidden'); + + // Cypress does not yet support the .hover() method and because of that we need to manually resize the element + // this is not ideal since it only resizes the cell not the entire column but it's enough to test the functionality + cy.get('#grid7-1 .slick-header-column:nth(0)') + // resize the 1st column + .each($elm => $elm.width(140)) + .find('.slick-resizable-handle') + .should('be.visible') + .invoke('show'); + + cy.get('#grid7-1 .slick-header-column:nth(0)') + .should(($el) => { + const expectedWidth = 140; // calculate with a calculated width including a (+/-)1px precision + expect($el.width()).greaterThan(expectedWidth - 1); + expect($el.width()).lessThan(expectedWidth + 1); + }); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button') + .should('have.length', 4); + }); + + it('should resize column to its previous size and still expect some icons to be hidden', () => { + cy.get('#grid7-1 .slick-header-column:nth(0)') + // resize the 1st column + .each($elm => $elm.width(50)) + .find('.slick-resizable-handle') + .should('be.visible') + .invoke('show'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(3)') + .should('be.hidden'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(1)') + .should('be.hidden'); + }); + + it('should go on the 2nd column "Hover me!" and expect the header button to appear only when doing hover over it', () => { + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(1)') + .should('contain', 'Hover me!'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(1)') + .find('.slick-header-button.slick-header-button-hidden') + .should('be.hidden') + .should('have.css', 'width', '0px'); + }); }); - it('should resize 1st column and make it wider', () => { - cy.get('.slick-viewport-top.slick-viewport-left') - .scrollTo('left') - .wait(50); - - const alertStub = cy.stub(); - cy.on('window:alert', alertStub); - - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(0)') - .should('contain', 'Resize me!'); - - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(0)') - .find('.slick-header-button:nth(3)') - .should('be.hidden'); - - // Cypress does not yet support the .hover() method and because of that we need to manually resize the element - // this is not ideal since it only resizes the cell not the entire column but it's enough to test the functionality - cy.get('.slick-header-column:nth(0)') - // resize the 1st column - .each($elm => $elm.width(140)) - .find('.slick-resizable-handle') - .should('be.visible') - .invoke('show'); - - cy.get('.slick-header-column:nth(0)') - .should(($el) => { - const expectedWidth = 140; // calculate with a calculated width including a (+/-)1px precision - expect($el.width()).greaterThan(expectedWidth - 1); - expect($el.width()).lessThan(expectedWidth + 1); + describe('Grid 2', () => { + it('should have exact Column Titles in the grid', () => { + cy.get('#grid7-2 .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should go over the 3rd column "Column C" and expect to see negative number in red after clicking on the red header button', () => { + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .should('contain', 'Column C'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-header-button.fa.fa-circle-o.red.faded') + .click({ force: true }); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-header-button.fa.fa-circle-o.red.faded') + .should('not.exist'); // shouldn't be faded anymore + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith(`execute a callback action to "toggle-highlight" on Column C`); }); - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(0)') - .find('.slick-header-button') - .should('have.length', 4); - }); + cy.get('#grid7-2 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; // check only the first 10 rows is enough + } + cy.wrap($row).children('.slick-cell:nth(2)') + .each($cell => { + const numberValue = $cell.text(); + const htmlValue = $cell.html(); + if (+numberValue < 0) { + expect(htmlValue).to.eq(`
      ${numberValue}
      `); + } else { + expect(htmlValue).to.eq(numberValue); + } + }); + }); + }); - it('should go on the 2nd column "Hover me!" and expect the header button to appear only when doing hover over it', () => { - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(1)') - .should('contain', 'Hover me!'); - - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(1)') - .find('.slick-header-button.slick-header-button-hidden') - .should('be.hidden') - .should('have.css', 'width', '0px'); - - // hover is not yet implemented in Cypress, so the following test won't work until then - // cy.get('.slick-header-columns') - // .children('.slick-header-column:nth(1)') - // .trigger('mouseover') - // .hover() - // .find('.slick-header-button') - // .should('have.css', 'width', '15px'); + it('should go over the 5th column "Column E" and not find the red header button', () => { + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(4)') + .should('contain', 'Column E'); + + // column E should not have the icon + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(4)') + .find('.slick-header-button') + .should('not.exist'); + }); + + it('should go over the last "Column J" and expect to find the button to have the disabled class and clicking it should not turn the negative numbers to red neither expect console log after clicking the disabled button', () => { + cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left') + .scrollTo('right') + .wait(50); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .should('contain', 'Column J') + .find('.slick-header-button-disabled') + .should('exist'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .find('.slick-header-button.slick-header-button-disabled.fa.fa-circle-o.red.faded') + .should('exist') + .click({ force: true }); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .find('.slick-header-button.slick-header-button-disabled.fa.fa-circle-o.red.faded') + .should('exist'); // should still be faded after previous click + + cy.get('#grid7-2 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; + } + cy.wrap($row).children('.slick-cell:nth(9)') + .each($cell => expect($cell.html()).to.eq($cell.text())); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(0); + }); + }); + + it('should resize 1st column and make it wider', () => { + cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left') + .scrollTo('left') + .wait(50); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .should('contain', 'Resize me!'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(3)') + .should('be.hidden'); + + // Cypress does not yet support the .hover() method and because of that we need to manually resize the element + // this is not ideal since it only resizes the cell not the entire column but it's enough to test the functionality + cy.get('#grid7-2 .slick-header-column:nth(0)') + // resize the 1st column + .each($elm => $elm.width(140)) + .find('.slick-resizable-handle') + .should('be.visible') + .invoke('show'); + + cy.get('#grid7-2 .slick-header-column:nth(0)') + .should(($el) => { + const expectedWidth = 140; // calculate with a calculated width including a (+/-)1px precision + expect($el.width()).greaterThan(expectedWidth - 1); + expect($el.width()).lessThan(expectedWidth + 1); + }); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button') + .should('have.length', 4); + }); + + it('should resize column to its previous size and still expect some icons to be hidden', () => { + cy.get('#grid7-2 .slick-header-column:nth(0)') + // resize the 1st column + .each($elm => $elm.width(50)) + .find('.slick-resizable-handle') + .should('be.visible') + .invoke('show'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(3)') + .should('be.hidden'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(1)') + .should('be.hidden'); + }); + + it('should go on the 2nd column "Hover me!" and expect the header button to appear only when doing hover over it', () => { + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(1)') + .should('contain', 'Hover me!'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(1)') + .find('.slick-header-button.slick-header-button-hidden') + .should('be.hidden') + .should('have.css', 'width', '0px'); + }); + + it('should filter "Column C" with positive number only and not expect any more red values', () => { + cy.get('#grid7-2 .search-filter.filter-2') + .type('>0'); + + cy.get('#grid7-2 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; // check only the first 10 rows is enough + } + cy.wrap($row).children('.slick-cell:nth(2)') + .each($cell => { + const numberValue = $cell.text(); + const htmlValue = $cell.html(); + expect(+numberValue).to.be.greaterThan(0); + }); + }); + }); + + it('should hover over the "Column C" and click on "Clear Filter" and expect grid to have all rows shown', () => { + cy.get('#grid7-2 .slick-header-column:nth(2)') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('#grid7-2 .slick-header-menu') + .should('be.visible') + .children('.slick-header-menu-item:nth-child(6)') + .children('.slick-header-menu-content') + .should('contain', 'Remove Filter') + .click(); + + cy.get('.slick-row').should('have.length.greaterThan', 1); + }); + + it('should Clear all Sorting', () => { + cy.get('#grid7-2') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item:nth(1)') + .find('span') + .contains('Clear all Sorting') + .click(); + }); + + it('should hover over the "Column C" and click on "Sort Ascending"', () => { + cy.get('#grid7-2 .slick-header-column:nth(2)') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .click(); + + cy.get('#grid7-2 .slick-header-menu') + .should('be.visible') + .children('.slick-header-menu-item:nth-child(3)') + .children('.slick-header-menu-content') + .should('contain', 'Sort Ascending') + .click(); + }); + + it('should expect first few items of "Column C" to be negative numbers and be red', () => { + cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left') + .scrollTo('top') + .wait(50); + + cy.get('#grid7-2 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; // check only the first 10 rows is enough + } + cy.wrap($row).children('.slick-cell:nth(2)') + .each($cell => { + const numberValue = $cell.text(); + const htmlValue = $cell.html(); + expect(htmlValue).to.eq(`
      ${numberValue}
      `); + }); + }); + }); }); -}); +}); \ No newline at end of file diff --git a/test/cypress/integration/example08.spec.js b/test/cypress/integration/example08.spec.js index a4e21801a..0fe7236e7 100644 --- a/test/cypress/integration/example08.spec.js +++ b/test/cypress/integration/example08.spec.js @@ -27,16 +27,16 @@ describe('Example 8 - Header Menu Plugin', { retries: 1 }, () => { .find('.slick-header-column') .first() .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); - cy.get('.slick-header-menuitem.slick-header-menuitem-disabled') + cy.get('.slick-header-menu-item.slick-header-menu-item-disabled') .contains('Help') .should('exist'); - cy.get('.slick-header-menuitem .slick-header-menucontent') + cy.get('.slick-header-menu-item .slick-header-menu-content') .contains('Hide Column') .should('exist'); }); @@ -48,13 +48,13 @@ describe('Example 8 - Header Menu Plugin', { retries: 1 }, () => { cy.get('#grid8') .find('.slick-header-column:nth(1)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); - cy.get('.slick-header-menuitem.bold') - .find('.slick-header-menucontent.blue') + cy.get('.slick-header-menu-item.bold') + .find('.slick-header-menu-content.blue') .contains('Help') .click() .then(() => expect(alertStub.getCall(0)).to.be.calledWith('Please help!!!')) @@ -69,11 +69,11 @@ describe('Example 8 - Header Menu Plugin', { retries: 1 }, () => { cy.get('#grid8') .find('.slick-header-column:nth(1)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .invoke('show') .click(); - cy.get('.slick-header-menuitem .slick-header-menucontent') + cy.get('.slick-header-menu-item .slick-header-menu-content') .contains('Sort Ascending') .click(); @@ -86,7 +86,7 @@ describe('Example 8 - Header Menu Plugin', { retries: 1 }, () => { cy.get('#grid8') .find('.slick-header-column:nth(2)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); @@ -94,7 +94,7 @@ describe('Example 8 - Header Menu Plugin', { retries: 1 }, () => { cy.get('.slick-header-menu') .should('exist'); - cy.get('.slick-header-menuitem .slick-header-menucontent') + cy.get('.slick-header-menu-item .slick-header-menu-content') .contains('Help') .should('not.exist'); }); @@ -103,7 +103,7 @@ describe('Example 8 - Header Menu Plugin', { retries: 1 }, () => { cy.get('.slick-header-menu') .should('exist'); - cy.get('.slick-header-menuitem .slick-header-menucontent') + cy.get('.slick-header-menu-item .slick-header-menu-content') .contains('Sort Descending') .click() .wait(10); @@ -131,12 +131,12 @@ describe('Example 8 - Header Menu Plugin', { retries: 1 }, () => { cy.get('#grid8') .find('.slick-header-column:nth(5)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); - cy.get('.slick-header-menuitem.slick-header-menuitem-disabled') + cy.get('.slick-header-menu-item.slick-header-menu-item-disabled') .contains('Help') .should('exist'); }); @@ -144,7 +144,7 @@ describe('Example 8 - Header Menu Plugin', { retries: 1 }, () => { it('should remain in the "Completed" column and execute "Hide Column" command and expect it gone from the grid', () => { const titles = ['Title', 'Duration', '% Complete', 'Start', 'Finish']; - cy.get('.slick-header-menuitem.slick-header-menuitem') + cy.get('.slick-header-menu-item.slick-header-menu-item') .contains('Hide Column') .click(); diff --git a/test/cypress/integration/example09.spec.js b/test/cypress/integration/example09.spec.js index aa1daa29d..19d02454c 100644 --- a/test/cypress/integration/example09.spec.js +++ b/test/cypress/integration/example09.spec.js @@ -26,20 +26,20 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { it('should open the Grid Menu and expect a title for "Custom Menus" and for "Columns"', () => { cy.get('#grid9') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click({ force: true }); - cy.get('.slick-gridmenu-custom') + cy.get('.slick-grid-menu-command-list') .find('.title') .contains('Custom Commands'); - cy.get('.slick-gridmenu') + cy.get('.slick-grid-menu') .find('.title') .contains('Columns'); cy.get('#grid9') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .trigger('click') .click({ force: true }); @@ -52,15 +52,15 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { .find('.slick-header-column') .first() .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .trigger('click', { force: true }); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(4)') + .children('.slick-header-menu-content') .should('contain', 'Hide Column') .click({ force: true }); @@ -75,18 +75,18 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { cy.on('window:alert', alertStub); cy.get('#grid9') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click({ force: true }); - cy.get('.slick-gridmenu-item.orange') - .find('.slick-gridmenu-content') + cy.get('.slick-grid-menu-item.orange') + .find('.slick-grid-menu-content') .contains('Command 1') .click() .then(() => expect(alertStub.getCall(0)).to.be.null); cy.get('#grid9') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .trigger('click') .click({ force: true }); @@ -97,22 +97,22 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { .type('10'); cy.get('#grid9') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click({ force: true }); - cy.get('.slick-gridmenu-item.red') + cy.get('.slick-grid-menu-item.red') .should('not.exist'); }); it('should clear all filters and expect no filters in the grid', () => { cy.get('#grid9') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click({ force: true }); - cy.get('.slick-gridmenu-item') - .find('.slick-gridmenu-content') + cy.get('.slick-grid-menu-item') + .find('.slick-grid-menu-content') .contains('Clear all Filters') .click(); @@ -122,31 +122,31 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { it('should clear the filters and then open the Grid Menu and expect the "Command 2" to now be visible', () => { cy.get('#grid9') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click({ force: true }); - cy.get('.slick-gridmenu-item.red') - .find('.slick-gridmenu-content.italic') + cy.get('.slick-grid-menu-item.red') + .find('.slick-grid-menu-content.italic') .should('contain', 'Command 2'); }); it('should click on the Grid Menu to show the Title as 1st column again', () => { cy.get('#grid9') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click({ force: true }); cy.get('#grid9') - .get('.slick-gridmenu:visible') - .find('.slick-gridmenu-list') + .get('.slick-grid-menu:visible') + .find('.slick-grid-menu-list') .children('li:nth-child(1)') .children('label') .should('contain', 'Title') .click({ force: true }); cy.get('#grid9') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .trigger('click') .click({ force: true }); @@ -162,12 +162,12 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { cy.on('window:alert', alertStub); cy.get('#grid9') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click({ force: true }); - cy.get('.slick-gridmenu-item.orange') - .find('.slick-gridmenu-content') + cy.get('.slick-grid-menu-item.orange') + .find('.slick-grid-menu-content') .contains('Command 1') .click() .then(() => expect(alertStub.getCall(0)).to.be.calledWith('command1')); @@ -180,15 +180,15 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { .find('.slick-header-column') .first() .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .trigger('click', { force: true }); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(4)') + .children('.slick-header-menu-content') .should('contain', 'Hide Column') .click({ force: true }); @@ -204,15 +204,15 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { .click({ force: true }); cy.get('#grid9') - .get('.slick-gridmenu:visible') - .find('.slick-gridmenu-list') + .get('.slick-grid-menu:visible') + .find('.slick-grid-menu-list') .children('li:nth-child(1)') .children('label') .should('contain', 'Title') .click({ force: true }); cy.get('#grid9') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .trigger('click') .click({ force: true }); @@ -223,7 +223,7 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { .each(($child, index) => expect($child.text()).to.eq(fullEnglishTitles[index])); cy.get('#grid9') - .get('.slick-gridmenu') + .get('.slick-grid-menu') .find('span.close') .trigger('click', { force: true }) .click({ force: true }); @@ -251,15 +251,15 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { .find('.slick-header-column') .first() .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .trigger('click', { force: true }); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(4)') + .children('.slick-header-menu-content') .should('contain', 'Cacher la colonne') .click({ force: true }); @@ -271,13 +271,13 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { it('should click on the Grid Menu to show the Title as 1st column again', () => { cy.get('#grid9') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click({ force: true }); cy.get('#grid9') - .get('.slick-gridmenu:visible') - .find('.slick-gridmenu-list') + .get('.slick-grid-menu:visible') + .find('.slick-grid-menu-list') .children('li:nth-child(1)') .children('label') .should('contain', 'Titre') @@ -296,15 +296,15 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { .find('.slick-header-column') .first() .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .trigger('click', { force: true }); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(4)') + .children('.slick-header-menu-content') .should('contain', 'Cacher la colonne') .click({ force: true }); @@ -320,15 +320,15 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { .click({ force: true }); cy.get('#grid9') - .get('.slick-gridmenu:visible') - .find('.slick-gridmenu-list') + .get('.slick-grid-menu:visible') + .find('.slick-grid-menu-list') .children('li:nth-child(1)') .children('label') .should('contain', 'Titre') .click({ force: true }); cy.get('#grid9') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .trigger('click') .click({ force: true }); @@ -339,7 +339,7 @@ describe('Example 9 - Grid Menu', { retries: 1 }, () => { .each(($child, index) => expect($child.text()).to.eq(fullFrenchTitles[index])); cy.get('#grid9') - .get('.slick-gridmenu') + .get('.slick-grid-menu') .find('span.close') .trigger('click', { force: true }) .click({ force: true }); diff --git a/test/cypress/integration/example10.spec.js b/test/cypress/integration/example10.spec.js index e5aef922e..4442c16ee 100644 --- a/test/cypress/integration/example10.spec.js +++ b/test/cypress/integration/example10.spec.js @@ -70,12 +70,12 @@ describe('Example 10 - Multiple Grids with Row Selection', { retries: 1 }, () => it('should make sure that first column is hidden from the Grid Menu (1st column definition has "excludeFromGridMenu" set) on 1st grid', () => { cy.get('#grid1') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get('.slick-gridmenu') - .find('.slick-gridmenu-list') + cy.get('.slick-grid-menu') + .find('.slick-grid-menu-list') .children() .each(($child, index) => { if (index === 0) { @@ -91,8 +91,8 @@ describe('Example 10 - Multiple Grids with Row Selection', { retries: 1 }, () => const newTitleList = ['', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; cy.get('#grid1') - .get('.slick-gridmenu:visible') - .find('.slick-gridmenu-list') + .get('.slick-grid-menu:visible') + .find('.slick-grid-menu-list') .children('li:nth-child(2)') .children('label') .should('contain', 'Title') @@ -106,7 +106,7 @@ describe('Example 10 - Multiple Grids with Row Selection', { retries: 1 }, () => }); cy.get('#grid1') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .trigger('click') .click(); @@ -156,12 +156,12 @@ describe('Example 10 - Multiple Grids with Row Selection', { retries: 1 }, () => describe('Pagination', () => { it('should Clear all Filters on 2nd Grid', () => { cy.get('#grid2') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') diff --git a/test/cypress/integration/example12.spec.js b/test/cypress/integration/example12.spec.js index 11a95a1cd..8a2ee6313 100644 --- a/test/cypress/integration/example12.spec.js +++ b/test/cypress/integration/example12.spec.js @@ -83,11 +83,11 @@ describe('Example 12: Localization (i18n)', { retries: 1 }, () => { describe('French locale', () => { it('should reset filters and switch locale to French', () => { cy.get('#grid12') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') @@ -152,11 +152,11 @@ describe('Example 12: Localization (i18n)', { retries: 1 }, () => { it('should reset filters before filtering duration', () => { cy.get('#grid12') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Supprimer tous les filtres') @@ -207,11 +207,11 @@ describe('Example 12: Localization (i18n)', { retries: 1 }, () => { .should('contain', 'en.json'); cy.get('#grid12') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') @@ -222,15 +222,15 @@ describe('Example 12: Localization (i18n)', { retries: 1 }, () => { cy.get('#slickGridContainer-grid12') .find('.slick-header-column:nth(1)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(4)') + .children('.slick-header-menu-content') .should('contain', 'Sort Descending') .click(); diff --git a/test/cypress/integration/example15.spec.js b/test/cypress/integration/example15.spec.js index f4d7e2845..75ae1ef67 100644 --- a/test/cypress/integration/example15.spec.js +++ b/test/cypress/integration/example15.spec.js @@ -103,7 +103,7 @@ describe('Example 15 - Column Span & Header Grouping', { retries: 1 }, () => { it('should click on the Grid Menu command "Unfreeze Columns/Rows" to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { cy.get('#grid2') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click({ force: true }); cy.contains('Unfreeze Columns/Rows') diff --git a/test/cypress/integration/example16.spec.js b/test/cypress/integration/example16.spec.js index 0dfa0178e..42634f8b8 100644 --- a/test/cypress/integration/example16.spec.js +++ b/test/cypress/integration/example16.spec.js @@ -171,15 +171,15 @@ describe('Example 16: Grid State & Presets using Local Storage', { retries: 1 }, cy.get('.slick-header-columns') .children('.slick-header-column:nth(2)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') // .should('be.hidden') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(5)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(5)') + .children('.slick-header-menu-content') .should('contain', 'Sort Descending') .click(); @@ -369,11 +369,11 @@ describe('Example 16: Grid State & Presets using Local Storage', { retries: 1 }, const expectedTitles = ['', 'Description', 'Durée', 'Titre', '% Achevée', 'Début', 'Terminé']; cy.get('#grid16') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click({ force: true }); - cy.get('.slick-gridmenu') - .find('.slick-gridmenu-list') + cy.get('.slick-grid-menu') + .find('.slick-grid-menu-list') .children() .each(($child, index) => { if (index === 0) { @@ -385,7 +385,7 @@ describe('Example 16: Grid State & Presets using Local Storage', { retries: 1 }, expect($child.text()).to.eq(expectedTitles[index]); }); - cy.get('.slick-gridmenu:visible') + cy.get('.slick-grid-menu:visible') .find('span.close') .trigger('click') .click(); @@ -397,15 +397,15 @@ describe('Example 16: Grid State & Presets using Local Storage', { retries: 1 }, cy.get('.slick-header-columns') .children('.slick-header-column:nth(5)') .trigger('mouseover', { force: true }) - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(9)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(9)') + .children('.slick-header-menu-content') .should('contain', 'Cacher la colonne') .click(); @@ -419,15 +419,15 @@ describe('Example 16: Grid State & Presets using Local Storage', { retries: 1 }, cy.get('.slick-header-columns') .children('.slick-header-column:nth(1)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(1)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(1)') + .children('.slick-header-menu-content') .should('contain', 'Geler les colonnes') .click(); }); diff --git a/test/cypress/integration/example17.spec.js b/test/cypress/integration/example17.spec.js index c2d70a7a3..75fc52e31 100644 --- a/test/cypress/integration/example17.spec.js +++ b/test/cypress/integration/example17.spec.js @@ -158,12 +158,12 @@ describe('Example 17 - Row Move & Checkbox Selector Selector Plugins', { retries const expectedFullHeaderMenuCommands = ['Clear all Filters', 'Clear all Sorting', 'Toggle Filter Row', 'Export to Excel']; cy.get('#grid17') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click({ force: true }); - cy.get('.slick-gridmenu-custom') - .find('.slick-gridmenu-item') + cy.get('.slick-grid-menu-command-list') + .find('.slick-grid-menu-item') .each(($child, index) => { const commandTitle = $child.text(); expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); @@ -207,7 +207,7 @@ describe('Example 17 - Row Move & Checkbox Selector Selector Plugins', { retries cy.get('#grid17') .find('.slick-header-column:nth(5)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); @@ -229,12 +229,12 @@ describe('Example 17 - Row Move & Checkbox Selector Selector Plugins', { retries const expectedFullHeaderMenuCommands = ['Clear all Filters', 'Clear all Sorting', 'Toggle Filter Row', 'Export to Excel']; cy.get('#grid17') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get('.slick-gridmenu-custom') - .find('.slick-gridmenu-item') + cy.get('.slick-grid-menu-command-list') + .find('.slick-grid-menu-item') .each(($child, index) => { const commandTitle = $child.text(); expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); @@ -256,7 +256,7 @@ describe('Example 17 - Row Move & Checkbox Selector Selector Plugins', { retries cy.get('#grid17') .find('.slick-header-column:nth(5)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); @@ -274,12 +274,12 @@ describe('Example 17 - Row Move & Checkbox Selector Selector Plugins', { retries const expectedFullHeaderMenuCommands = ['Clear all Filters', 'Clear all Sorting', 'Toggle Filter Row', 'Export to Excel']; cy.get('#grid17') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get('.slick-gridmenu-custom') - .find('.slick-gridmenu-item') + cy.get('.slick-grid-menu-command-list') + .find('.slick-grid-menu-item') .each(($child, index) => { const commandTitle = $child.text(); expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); @@ -301,7 +301,7 @@ describe('Example 17 - Row Move & Checkbox Selector Selector Plugins', { retries cy.get('#grid17') .find('.slick-header-column:nth(5)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); @@ -327,7 +327,7 @@ describe('Example 17 - Row Move & Checkbox Selector Selector Plugins', { retries cy.get('#grid17') .find('.slick-header-column:nth(5)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); @@ -349,12 +349,12 @@ describe('Example 17 - Row Move & Checkbox Selector Selector Plugins', { retries const expectedFullHeaderMenuCommands = ['Clear all Filters', 'Clear all Sorting', 'Toggle Filter Row', 'Export to Excel']; cy.get('#grid17') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get('.slick-gridmenu-custom') - .find('.slick-gridmenu-item') + cy.get('.slick-grid-menu-command-list') + .find('.slick-grid-menu-item') .each(($child, index) => { const commandTitle = $child.text(); expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); diff --git a/test/cypress/integration/example19.spec.js b/test/cypress/integration/example19.spec.js index 676448c5e..2695b4acb 100644 --- a/test/cypress/integration/example19.spec.js +++ b/test/cypress/integration/example19.spec.js @@ -1,7 +1,7 @@ /// describe('Example 19 - Draggable Grouping & Aggregators', { retries: 1 }, () => { - const fullTitles = ['Title', 'Duration', '% Complete', 'Start', 'Finish', 'Cost', 'Effort Driven']; + const fullTitles = ['Title', 'Duration', '% Complete', 'Start', 'Finish', 'Cost', 'Effort-Driven']; const GRID_ROW_HEIGHT = 35; it('should display Example title', () => { @@ -60,18 +60,18 @@ describe('Example 19 - Draggable Grouping & Aggregators', { retries: 1 }, () => cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('contain', '0'); }); - it('should show 2 column titles (Duration, Effort Driven) shown in the pre-header section', () => { + it('should show 2 column titles (Duration, Effort-Driven) shown in the pre-header section', () => { cy.get('.slick-dropped-grouping:nth(0) div').contains('Duration'); - cy.get('.slick-dropped-grouping:nth(1) div').contains('Effort Driven'); + cy.get('.slick-dropped-grouping:nth(1) div').contains('Effort-Driven'); }); it('should be able to drag and swap grouped column titles inside the pre-header', () => { cy.get('.slick-dropped-grouping:nth(0) div') .contains('Duration') - .trigger('mousedown', 'bottom', { which: 1 }); + .trigger('mousedown', 'center', { which: 1 }); cy.get('.slick-dropped-grouping:nth(1) div') - .contains('Effort Driven') + .contains('Effort-Driven') .trigger('mousemove', 'bottomRight') .trigger('mouseup', 'bottomRight', { force: true }); }); @@ -86,5 +86,100 @@ describe('Example 19 - Draggable Grouping & Aggregators', { retries: 1 }, () => cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0)`).should('contain', 'Task'); cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('contain', '0'); }); + + it('should expand all rows with "Expand All" from context menu and expect all the Groups to be expanded and the Toogle All icon to be collapsed', () => { + cy.get('#grid19') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-context-menu-command-list') + .find('.slick-context-menu-item') + .find('.slick-context-menu-content') + .contains('Expand all Groups') + .click(); + + cy.get('#grid19') + .find('.slick-group-toggle.collapsed') + .should('have.length', 0); + + cy.get('#grid19') + .find('.slick-group-toggle.expanded') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + + cy.get('.slick-group-toggle-all-icon.expanded') + .should('exist'); + }); + + it('should collapse all rows with "Collapse All" from context menu and expect all the Groups to be collapsed and the Toogle All icon to be collapsed', () => { + cy.get('#grid19') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-context-menu-command-list') + .find('.slick-context-menu-item') + .find('.slick-context-menu-content') + .contains('Collapse all Groups') + .click(); + + cy.get('#grid19') + .find('.slick-group-toggle.expanded') + .should('have.length', 0); + + cy.get('#grid19') + .find('.slick-group-toggle.collapsed') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + + cy.get('.slick-group-toggle-all-icon.collapsed') + .should('exist'); + }); + + it('should use the preheader Toggle All button and expect all groups to now be expanded', () => { + cy.get('.slick-preheader-panel .slick-group-toggle-all').click(); + + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: False'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0) .slick-group-toggle.expanded`) + .should('have.css', 'marginLeft').and('eq', `0px`); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0) .slick-group-toggle.expanded`) + .should('have.css', 'marginLeft').and('eq', `15px`); + }); + + it('should use the preheader Toggle All button again and expect all groups to now be collapsed', () => { + cy.get('.slick-preheader-panel .slick-group-toggle-all').click(); + + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: False'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: True'); + }); + + it('should clear all groups with "Clear all Grouping" from context menu and expect all the Groups to be collapsed and the Toogle All icon to be collapsed', () => { + cy.get('#grid19') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-context-menu-command-list') + .find('.slick-context-menu-item') + .find('.slick-context-menu-content') + .contains('Clear all Grouping') + .click(); + + cy.get('#grid19') + .find('.slick-group-toggle-all') + .should('be.hidden'); + + cy.get('#grid19') + .find('.slick-draggable-dropbox-toggle-placeholder') + .should('be.visible') + .should('have.text', 'Drop a column header here to group by the column'); + }); + + it('should add 500 items and expect 500 of 500 items displayed', () => { + cy.get('[data-test="add-500-rows-btn"]') + .click(); + + cy.get('.right-footer') + .contains('500 of 500 items'); + }); }); -}); +}); \ No newline at end of file diff --git a/test/cypress/integration/example20.spec.js b/test/cypress/integration/example20.spec.js index c4b7213ff..64aa084da 100644 --- a/test/cypress/integration/example20.spec.js +++ b/test/cypress/integration/example20.spec.js @@ -40,12 +40,12 @@ describe('Example 20 - Frozen Grid', { retries: 1 }, () => { const newColumnList = ['#', '% Complete', 'Start', 'Finish', 'Cost | Duration', 'Effort Driven', 'Title 1', 'Title 2', 'Title 3', 'Title 4']; cy.get('#grid20') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click({ force: true }); cy.get('#grid20') - .get('.slick-gridmenu:visible') - .find('.slick-gridmenu-list') + .get('.slick-grid-menu:visible') + .find('.slick-grid-menu-list') .children('li:visible:nth(1)') .children('label') .should('contain', 'Title') @@ -67,15 +67,15 @@ describe('Example 20 - Frozen Grid', { retries: 1 }, () => { it('should show again "Title" column from Grid Menu and expect last frozen column to still be "% Complete"', () => { cy.get('#grid20') - .get('.slick-gridmenu:visible') - .find('.slick-gridmenu-list') + .get('.slick-grid-menu:visible') + .find('.slick-grid-menu-list') .children('li:visible:nth(1)') .children('label') .should('contain', 'Title') .click({ force: true }); cy.get('#grid20') - .get('.slick-gridmenu:visible') + .get('.slick-grid-menu:visible') .find('span.close') .click({ force: true }); @@ -100,15 +100,15 @@ describe('Example 20 - Frozen Grid', { retries: 1 }, () => { cy.get('#grid20') .find('.slick-header-column:nth(1)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(8)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(8)') + .children('.slick-header-menu-content') .should('contain', 'Hide Column') .click(); @@ -205,7 +205,7 @@ describe('Example 20 - Frozen Grid', { retries: 1 }, () => { it('should click on the Grid Menu command "Unfreeze Columns/Rows" to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { cy.get('#grid20') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click({ force: true }); cy.contains('Unfreeze Columns/Rows') diff --git a/test/cypress/integration/example21.spec.js b/test/cypress/integration/example21.spec.js index 40a2ea3a4..b55561976 100644 --- a/test/cypress/integration/example21.spec.js +++ b/test/cypress/integration/example21.spec.js @@ -195,29 +195,29 @@ describe('Example 21 - Row Detail View', { retries: 1 }, () => { cy.get('#slickGridContainer-grid21') .find('.slick-header-column:nth(1)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .should('be.hidden') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(4)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(4)') + .children('.slick-header-menu-content') .should('contain', 'Sort Descending') .click(); cy.get('#slickGridContainer-grid21') .find('.slick-header-column:nth(1)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(3)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(3)') + .children('.slick-header-menu-content') .should('contain', 'Sort Ascending') .click(); diff --git a/test/cypress/integration/example27.spec.js b/test/cypress/integration/example27.spec.js index ca1a9a4af..c9bbafc38 100644 --- a/test/cypress/integration/example27.spec.js +++ b/test/cypress/integration/example27.spec.js @@ -111,11 +111,11 @@ describe('Example 27 - GraphQL Basic API without Pagination', { retries: 1 }, () it('should Clear all Filters and expect all rows to be back', () => { cy.get('#grid27') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') @@ -164,12 +164,12 @@ describe('Example 27 - GraphQL Basic API without Pagination', { retries: 1 }, () it('should Clear all Filters and expect all rows to be back', () => { cy.get('#grid27') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') diff --git a/test/cypress/integration/example28.spec.js b/test/cypress/integration/example28.spec.js index 9a5fd4639..431750a36 100644 --- a/test/cypress/integration/example28.spec.js +++ b/test/cypress/integration/example28.spec.js @@ -134,12 +134,12 @@ describe('Example 28 - Tree Data (from a flat dataset with parentId references)' it('should open the Grid Menu "Clear all Filters" command', () => { cy.get('#grid28') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') @@ -186,12 +186,12 @@ describe('Example 28 - Tree Data (from a flat dataset with parentId references)' it('should open the Grid Menu "Clear all Filters" command', () => { cy.get('#grid28') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') @@ -223,12 +223,12 @@ describe('Example 28 - Tree Data (from a flat dataset with parentId references)' it('should open the Grid Menu "Clear all Filters" command', () => { cy.get('#grid28') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') diff --git a/test/cypress/integration/example29.spec.js b/test/cypress/integration/example29.spec.js index 329514b0f..33572fddf 100644 --- a/test/cypress/integration/example29.spec.js +++ b/test/cypress/integration/example29.spec.js @@ -134,12 +134,12 @@ describe('Example 29 - Tree Data (from a Hierarchical Dataset)', { retries: 1 }, it('should Clear all Filters and default list', () => { cy.get('#slickGridContainer-grid29') - .find('button.slick-gridmenu-button') + .find('button.slick-grid-menu-button') .trigger('click') .click(); - cy.get(`.slick-gridmenu:visible`) - .find('.slick-gridmenu-item') + cy.get(`.slick-grid-menu:visible`) + .find('.slick-grid-menu-item') .first() .find('span') .contains('Clear all Filters') diff --git a/test/cypress/integration/example31.spec.js b/test/cypress/integration/example31.spec.js index 00c207cd6..52cd9ff36 100644 --- a/test/cypress/integration/example31.spec.js +++ b/test/cypress/integration/example31.spec.js @@ -69,14 +69,14 @@ describe('Example 31 - Columns Resize by Content', { retries: 1 }, () => { cy.get('#grid31') .find('.slick-header-column:nth-child(10)') .trigger('mouseover') - .children('.slick-header-menubutton') + .children('.slick-header-menu-button') .invoke('show') .click(); cy.get('.slick-header-menu') .should('be.visible') - .children('.slick-header-menuitem:nth-child(1)') - .children('.slick-header-menucontent') + .children('.slick-header-menu-item:nth-child(1)') + .children('.slick-header-menu-content') .should('contain', 'Resize by Content') .click(); diff --git a/test/rxjsResourceStub.ts b/test/rxjsResourceStub.ts index 1adace899..13a56b977 100644 --- a/test/rxjsResourceStub.ts +++ b/test/rxjsResourceStub.ts @@ -1,5 +1,5 @@ import { RxJsFacade } from '@slickgrid-universal/common'; -import { EMPTY, iif, isObservable, firstValueFrom, Observable, Subject } from 'rxjs'; +import { EMPTY, iif, isObservable, firstValueFrom, Observable, ObservableInput, of, OperatorFunction, ObservedValueOf, Subject, switchMap, } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; export class RxJsResourceStub implements RxJsFacade { @@ -18,12 +18,12 @@ export class RxJsResourceStub implements RxJsFacade { return new Observable(); } - /** Simple method to create an Subject */ + /** Simple method to create a Subject */ createSubject(): Subject { return new Subject(); } - /** he same Observable instance returned by any call to without a scheduler. It is preferrable to use this over empty() */ + /** same Observable instance returned by any call to without a scheduler. It is preferrable to use this over empty() */ empty(): Observable { return EMPTY; } @@ -41,6 +41,15 @@ export class RxJsResourceStub implements RxJsFacade { return isObservable(obj); } + /** Converts the arguments to an observable sequence. */ + of(value: any): Observable { + return of(value); + } + + switchMap>(project: (value: T, index: number) => O): OperatorFunction> { + return switchMap(project); + } + /** Emits the values emitted by the source Observable until a `notifier` Observable emits a value. */ takeUntil(notifier: Observable): any { return takeUntil(notifier);