diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example01.html b/examples/webpack-demo-vanilla-bundle/src/examples/example01.html index 4ea345f05..ae72c28cb 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example01.html +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example01.html @@ -28,6 +28,11 @@
Grid 2
Toggle Pagination +

diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example01.scss b/examples/webpack-demo-vanilla-bundle/src/examples/example01.scss new file mode 100644 index 000000000..f0fd2f8f2 --- /dev/null +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example01.scss @@ -0,0 +1,3 @@ +.red { + color: #ff0000; +} \ No newline at end of file diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example01.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example01.ts index a6eff8397..a90213ca0 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example01.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example01.ts @@ -1,10 +1,11 @@ -import { Column, Formatters, GridOption } from '@slickgrid-universal/common'; +import { Column, ExtensionName, Formatters, GridOption } from '@slickgrid-universal/common'; import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; import { ExampleGridOptions } from './example-grid-options'; // use any of the Styling Theme // import '../material-styles.scss'; import '../salesforce-styles.scss'; +import './example01.scss'; const NB_ITEMS = 995; @@ -63,8 +64,31 @@ export class Example1 { ...this.gridOptions1, ...{ gridHeight: 255, - enablePagination: true, + columnPicker: { + onColumnsChanged: (e, args) => console.log('columnPicker:onColumnsChanged - visible columns count', args.visibleColumns.length), + }, + gridMenu: { + // customItems: [ + // { command: 'help', title: 'Help', positionOrder: 70, action: (e, args) => console.log(args) }, + // { command: '', divider: true, positionOrder: 72 }, + // { command: 'hello', title: 'Hello', positionOrder: 69, action: (e, args) => alert('Hello World'), cssClass: 'red', tooltip: 'Hello World', iconCssClass: 'mdi mdi-close' }, + // ], + alignDropSide: 'right', + // menuUsabilityOverride: () => false, + onBeforeMenuShow: () => { + console.log('gridMenu:onBeforeMenuShow'); + // return false; // returning false would prevent the grid menu from opening + }, + onAfterMenuShow: () => console.log('gridMenu:onAfterMenuShow'), + onColumnsChanged: (_e, args) => console.log('gridMenu:onColumnsChanged', args), + onCommand: (e, args) => { + // e.preventDefault(); // preventing default event would keep the menu open after the execution + console.log('gridMenu:onCommand', args.command); + }, + onMenuClose: (e, args) => console.log('gridMenu:onMenuClose - visible columns count', args.visibleColumns.length), + }, enableFiltering: true, + enablePagination: true, pagination: { pageSizes: [5, 10, 15, 20, 25, 50, 75, 100], pageSize: 5 @@ -114,4 +138,13 @@ export class Example1 { this.isGrid2WithPagination = !this.isGrid2WithPagination; this.sgb2.paginationService!.togglePaginationVisibility(this.isGrid2WithPagination); } + + toggleGridMenu(e: Event) { + if (this.sgb2?.extensionService) { + const gridMenuInstance = this.sgb2.extensionService.getSlickgridAddonInstance(ExtensionName.gridMenu); + // 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 left without affecting the menu within which will stay aligned on the right + gridMenuInstance.showGridMenu(e, { alignDropSide: 'left' }); + } + } } diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example15.html b/examples/webpack-demo-vanilla-bundle/src/examples/example15.html index 4e415b4c4..036a94736 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example15.html +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example15.html @@ -15,13 +15,13 @@

Clear all Filter & Sorts - - - diff --git a/packages/common/src/controls/__tests__/columnPickerControl.spec.ts b/packages/common/src/controls/__tests__/columnPickerControl.spec.ts index 84276fd66..11dd0ed86 100644 --- a/packages/common/src/controls/__tests__/columnPickerControl.spec.ts +++ b/packages/common/src/controls/__tests__/columnPickerControl.spec.ts @@ -3,15 +3,18 @@ import { ColumnPickerControl } from '../columnPicker.control'; import { ExtensionUtility } from '../../extensions/extensionUtility'; import { SharedService } from '../../services/shared.service'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; +import { BackendUtilityService } from '../../services/backendUtility.service'; +import { PubSubService } from '../../services'; declare const Slick: SlickNamespace; +const gridUid = 'slickgrid_124343'; const gridStub = { getColumnIndex: jest.fn(), getColumns: jest.fn(), getOptions: jest.fn(), getSelectedRows: jest.fn(), - getUID: jest.fn(), + getUID: () => gridUid, registerPlugin: jest.fn(), setColumns: jest.fn(), setOptions: jest.fn(), @@ -20,6 +23,13 @@ const gridStub = { onHeaderContextMenu: new Slick.Event(), } as unknown as SlickGrid; +const pubSubServiceStub = { + publish: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + unsubscribeAll: jest.fn(), +} as PubSubService; + describe('ColumnPickerControl', () => { const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() }; const columnsMock: Column[] = [ @@ -29,6 +39,7 @@ describe('ColumnPickerControl', () => { ]; let control: ColumnPickerControl; + let backendUtilityService: BackendUtilityService; let sharedService: SharedService; let translateService: TranslateServiceStub; let extensionUtility: ExtensionUtility; @@ -45,8 +56,9 @@ describe('ColumnPickerControl', () => { beforeEach(() => { sharedService = new SharedService(); + backendUtilityService = new BackendUtilityService(); translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); @@ -56,7 +68,7 @@ describe('ColumnPickerControl', () => { jest.spyOn(gridStub, 'getColumns').mockReturnValue(columnsMock); jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); - control = new ColumnPickerControl(extensionUtility, sharedService); + control = new ColumnPickerControl(extensionUtility, pubSubServiceStub, sharedService); translateService.use('fr'); }); @@ -66,7 +78,7 @@ describe('ColumnPickerControl', () => { jest.clearAllMocks(); }); - describe('registered plugin', () => { + describe('registered control', () => { afterEach(() => { gridOptionsMock.columnPicker.headerColumnValueExtractor = null; gridOptionsMock.columnPicker.onColumnsChanged = null; @@ -91,7 +103,7 @@ describe('ColumnPickerControl', () => { const inputElm = control.menuElement.querySelector('input[type="checkbox"]'); inputElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); - expect(control.menuElement.style.display).toBe('block'); + expect(control.menuElement.style.visibility).toBe('visible'); expect(setSelectionSpy).toHaveBeenCalledWith(mockRowSelection); expect(control.getAllColumns()).toEqual(columnsMock); expect(control.getVisibleColumns()).toEqual(columnsMock); @@ -109,12 +121,12 @@ describe('ColumnPickerControl', () => { const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() }; gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub); - expect(control.menuElement.style.display).toBe('block'); + expect(control.menuElement.style.visibility).toBe('visible'); const bodyElm = document.body; bodyElm.dispatchEvent(new Event('mousedown', { bubbles: true })); - expect(control.menuElement.style.display).toBe('none'); + expect(control.menuElement.style.visibility).toBe('hidden'); }); it('should query an input checkbox change event and expect "readjustFrozenColumnIndexWhenNeeded" method to be called when the grid is detected to be a frozen grid', () => { @@ -140,10 +152,7 @@ describe('ColumnPickerControl', () => { jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); - gridOptionsMock.columnPicker.headerColumnValueExtractor = (column: Column) => { - const headerGroup = column?.columnGroup || ''; - return `${headerGroup} - ${column.name}`; - }; + gridOptionsMock.columnPicker.headerColumnValueExtractor = (column: Column) => `${column?.columnGroup || ''} - ${column.name}`; control.columns = columnsMock; control.init(); @@ -169,8 +178,8 @@ describe('ColumnPickerControl', () => { gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub); control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); - const inputForcefitElm = control.menuElement.querySelector('#colpicker-forcefit'); - const labelSyncElm = control.menuElement.querySelector('label[for=colpicker-forcefit]'); + const inputForcefitElm = control.menuElement.querySelector('#slickgrid_124343-colpicker-forcefit'); + const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-forcefit]'); expect(handlerSpy).toHaveBeenCalledTimes(2); expect(control.getAllColumns()).toEqual(columnsMock); @@ -191,8 +200,8 @@ describe('ColumnPickerControl', () => { gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub); control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); - const inputSyncElm = control.menuElement.querySelector('#colpicker-syncresize'); - const labelSyncElm = control.menuElement.querySelector('label[for=colpicker-syncresize]'); + const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-colpicker-syncresize'); + const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-syncresize]'); expect(handlerSpy).toHaveBeenCalledTimes(2); expect(control.getAllColumns()).toEqual(columnsMock); @@ -204,6 +213,7 @@ describe('ColumnPickerControl', () => { it('should open the column picker via "onHeaderContextMenu" and expect "onColumnsChanged" to be called when defined', () => { const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); const onColChangedMock = jest.fn(); jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); @@ -214,16 +224,19 @@ describe('ColumnPickerControl', () => { gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub); control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); - expect(handlerSpy).toHaveBeenCalledTimes(2); - expect(control.getAllColumns()).toEqual(columnsMock); - expect(control.getVisibleColumns()).toEqual(columnsMock); - expect(onColChangedMock).toBeCalledWith(expect.anything(), { + const expectedCallbackArgs = { columnId: 'field1', showing: true, allColumns: columnsMock, columns: columnsMock, + visibleColumns: columnsMock, grid: gridStub, - }); + }; + expect(handlerSpy).toHaveBeenCalledTimes(2); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + expect(onColChangedMock).toBeCalledWith(expect.anything(), expectedCallbackArgs); + expect(pubSubSpy).toHaveBeenCalledWith('columnPicker:onColumnsChanged', expectedCallbackArgs); }); it('should open the column picker via "onHeaderContextMenu", click on "Force Fit Columns" checkbox and expect "setOptions" and "setColumns" to be called with previous visible columns', () => { @@ -239,8 +252,8 @@ describe('ColumnPickerControl', () => { control.init(); gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub); - const inputForcefitElm = control.menuElement.querySelector('#colpicker-forcefit'); - const labelSyncElm = control.menuElement.querySelector('label[for=colpicker-forcefit]'); + const inputForcefitElm = control.menuElement.querySelector('#slickgrid_124343-colpicker-forcefit'); + const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-forcefit]'); inputForcefitElm.dispatchEvent(new Event('click', { bubbles: true })); expect(handlerSpy).toHaveBeenCalledTimes(2); @@ -265,8 +278,8 @@ describe('ColumnPickerControl', () => { control.init(); gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub); - const inputSyncElm = control.menuElement.querySelector('#colpicker-syncresize'); - const labelSyncElm = control.menuElement.querySelector('label[for=colpicker-syncresize]'); + const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-colpicker-syncresize'); + const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-syncresize]'); inputSyncElm.dispatchEvent(new Event('click', { bubbles: true })); expect(handlerSpy).toHaveBeenCalledTimes(2); @@ -289,7 +302,6 @@ describe('ColumnPickerControl', () => { { id: 'field2', field: 'field2', name: 'Field 2', width: 75 }, { id: 'field3', field: 'field3', name: 'Field 3', width: 75, columnGroup: 'Billing' }, ]; - jest.spyOn(control, 'getAllColumns').mockReturnValue(columnsMock); jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValueOnce(0).mockReturnValueOnce(1); const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); @@ -324,8 +336,8 @@ describe('ColumnPickerControl', () => { gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub); control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); - const labelForcefitElm = control.menuElement.querySelector('label[for=colpicker-forcefit]'); - const labelSyncElm = control.menuElement.querySelector('label[for=colpicker-syncresize]'); + const labelForcefitElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-forcefit]'); + const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-syncresize]'); expect(handlerSpy).toHaveBeenCalledTimes(2); expect(labelForcefitElm.textContent).toBe('Ajustement forcé des colonnes'); diff --git a/packages/common/src/controls/__tests__/gridMenuControl.spec.ts b/packages/common/src/controls/__tests__/gridMenuControl.spec.ts new file mode 100644 index 000000000..3cb903a98 --- /dev/null +++ b/packages/common/src/controls/__tests__/gridMenuControl.spec.ts @@ -0,0 +1,1449 @@ +import 'jest-extended'; +import { DelimiterType, FileType } from '../../enums/index'; +import { Column, DOMEvent, GridMenu, GridOption, SlickDataView, SlickGrid, SlickNamespace, } from '../../interfaces/index'; +import { GridMenuControl } from '../gridMenu.control'; +import { SharedService } from '../../services/shared.service'; +import { BackendUtilityService, ExcelExportService, FilterService, PubSubService, SortService, TextExportService, } from '../../services'; +import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; +import { ExtensionUtility } from '../../extensions/extensionUtility'; + +declare const Slick: SlickNamespace; +jest.mock('flatpickr', () => { }); + +const gridId = 'grid1'; +const gridUid = 'slickgrid_124343'; +const containerId = 'demo-container'; + +const excelExportServiceStub = { + className: 'ExcelExportService', + exportToExcel: jest.fn(), +} as unknown as ExcelExportService; + +const textExportServiceStub = { + className: 'TextExportService', + exportToFile: jest.fn(), +} as unknown as TextExportService; + +const filterServiceStub = { + clearFilters: jest.fn(), +} as unknown as FilterService; + +const pubSubServiceStub = { + publish: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + unsubscribeAll: jest.fn(), +} as PubSubService; + +const sortServiceStub = { + clearSorting: jest.fn(), +} as unknown as SortService; + +const dataViewStub = { + refresh: jest.fn(), +} as unknown as SlickDataView; + +const gridStub = { + autosizeColumns: jest.fn(), + getColumnIndex: jest.fn(), + getColumns: jest.fn(), + getOptions: jest.fn(), + getSelectedRows: jest.fn(), + getUID: () => gridUid, + registerPlugin: jest.fn(), + setColumns: jest.fn(), + setHeaderRowVisibility: jest.fn(), + setSelectedRows: jest.fn(), + setTopPanelVisibility: jest.fn(), + setPreHeaderPanelVisibility: jest.fn(), + setOptions: jest.fn(), + scrollColumnIntoView: jest.fn(), + onBeforeDestroy: new Slick.Event(), + onColumnsReordered: new Slick.Event(), + onSetOptions: new Slick.Event(), +} as unknown as SlickGrid; + +// define a
container to simulate the grid container +const template = + `
+
+
+
+
+
+
+
+
+
+
+
+
+
`; + +describe('GridMenuControl', () => { + let control: GridMenuControl; + const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() }; + const columnsMock: Column[] = [ + { id: 'field1', field: 'field1', name: 'Field 1', width: 100, nameKey: 'TITLE' }, + { id: 'field2', field: 'field2', name: 'Field 2', width: 75 }, + { id: 'field3', field: 'field3', name: 'Field 3', width: 75, columnGroup: 'Billing' }, + ]; + let backendUtilityService: BackendUtilityService; + let extensionUtility: ExtensionUtility; + let translateService: TranslateServiceStub; + let sharedService: SharedService; + + const gridMenuOptionsMock = { + commandLabels: { + clearAllFiltersCommandKey: 'CLEAR_ALL_FILTERS', + clearAllSortingCommandKey: 'CLEAR_ALL_SORTING', + clearFrozenColumnsCommandKey: 'CLEAR_PINNING', + exportCsvCommandKey: 'EXPORT_TO_CSV', + exportExcelCommandKey: 'EXPORT_TO_EXCEL', + exportTextDelimitedCommandKey: 'EXPORT_TO_TAB_DELIMITED', + refreshDatasetCommandKey: 'REFRESH_DATASET', + toggleFilterCommandKey: 'TOGGLE_FILTER_ROW', + togglePreHeaderCommandKey: 'TOGGLE_PRE_HEADER_ROW', + }, + customItems: [], + hideClearAllFiltersCommand: false, + hideClearFrozenColumnsCommand: true, + hideForceFitButton: false, + hideSyncResizeButton: true, + onExtensionRegistered: jest.fn(), + onCommand: () => { }, + onColumnsChanged: () => { }, + onAfterMenuShow: () => { }, + onBeforeMenuShow: () => { }, + onMenuClose: () => { }, + }; + const gridOptionsMock = { + enableAutoSizeColumns: true, + enableGridMenu: true, + enableTranslate: true, + backendServiceApi: { + service: { + buildQuery: jest.fn(), + }, + internalPostProcess: jest.fn(), + preProcess: jest.fn(), + process: jest.fn(), + postProcess: jest.fn(), + }, + gridMenu: gridMenuOptionsMock, + pagination: { + totalItems: 0 + }, + showHeaderRow: false, + showTopPanel: false, + showPreHeaderPanel: false + } as unknown as GridOption; + let div; + + describe('with I18N Service', () => { + const consoleErrorSpy = jest.spyOn(global.console, 'error').mockReturnValue(); + const consoleWarnSpy = jest.spyOn(global.console, 'warn').mockReturnValue(); + // let divElement; + + beforeEach(() => { + // divElement = document.createElement('div'); + div = document.createElement('div'); + div.innerHTML = template; + document.body.appendChild(div); + backendUtilityService = new BackendUtilityService(); + sharedService = new SharedService(); + translateService = new TranslateServiceStub(); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); + + jest.spyOn(gridStub, 'getColumns').mockReturnValue(columnsMock); + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + jest.spyOn(SharedService.prototype, 'dataView', 'get').mockReturnValue(dataViewStub); + jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(columnsMock); + jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(columnsMock.slice(0, 1)); + jest.spyOn(SharedService.prototype, 'columnDefinitions', 'get').mockReturnValue(columnsMock); + + control = new GridMenuControl(extensionUtility, filterServiceStub, pubSubServiceStub, sharedService, sortServiceStub); + translateService.use('fr'); + }); + + afterEach(() => { + control?.eventHandler.unsubscribeAll(); + control?.dispose(); + jest.clearAllMocks(); + }); + + describe('registered control', () => { + beforeEach(() => { + control.dispose(); + document.body.innerHTML = ''; + div = document.createElement('div'); + div.innerHTML = template; + document.body.appendChild(div); + }); + + afterEach(() => { + gridMenuOptionsMock.onBeforeMenuShow = undefined; + control?.eventHandler.unsubscribeAll(); + gridOptionsMock.gridMenu = gridMenuOptionsMock; + jest.clearAllMocks(); + control.dispose(); + }); + + it('should expect the Control to be created', () => { + expect(control).toBeTruthy(); + }); + + it('should query an input checkbox change event and expect "setSelectedRows" method to be called using Row Selection when enabled', () => { + const mockRowSelection = [0, 3, 5]; + jest.spyOn(control.eventHandler, 'subscribe'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue(mockRowSelection); + const setSelectionSpy = jest.spyOn(gridStub, 'setSelectedRows'); + + gridOptionsMock.enableRowSelection = true; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const inputElm = control.menuElement.querySelector('input[type="checkbox"]'); + inputElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + + expect(control.menuElement.style.visibility).toBe('visible'); + expect(setSelectionSpy).toHaveBeenCalledWith(mockRowSelection); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + }); + + it('should open the Grid Menu and then expect it to hide when clicking anywhere in the DOM body', () => { + const mockRowSelection = [0, 3, 5]; + jest.spyOn(control.eventHandler, 'subscribe'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue(mockRowSelection); + + gridOptionsMock.enableRowSelection = true; + gridOptionsMock.showHeaderRow = true; + gridOptionsMock.gridMenu.menuWidth = 16; + gridOptionsMock.gridMenu.resizeOnShowHeaderRow = true; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const headerRowElm = document.querySelector('.slick-headerrow') as HTMLDivElement; + + expect(control.menuElement.style.visibility).toBe('visible'); + expect(headerRowElm.style.width).toBe(`calc(100% - 16px)`) + + const bodyElm = document.body; + bodyElm.dispatchEvent(new Event('mousedown', { bubbles: true })); + + expect(control.menuElement.style.visibility).toBe('hidden'); + }); + + it('should query an input checkbox change event and expect "readjustFrozenColumnIndexWhenNeeded" method to be called when the grid is detected to be a frozen grid', () => { + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); + + gridOptionsMock.frozenColumn = 0; + control.columns = columnsMock; + control.initEventHandlers(); + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(readjustSpy).toHaveBeenCalledWith(0, columnsMock, columnsMock); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + }); + + it('should query an input checkbox change event and expect "readjustFrozenColumnIndexWhenNeeded" method to be called when the grid is detected to be a frozen grid', () => { + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); + + gridOptionsMock.frozenColumn = 0; + control.columns = columnsMock; + control.initEventHandlers(); + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(readjustSpy).toHaveBeenCalledWith(0, columnsMock, columnsMock); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + }); + + it('should expect the Grid Menu to change from the Left side container to the Right side when changing from a regular to a frozen grid via "setOptions"', () => { + const recreateSpy = jest.spyOn(control, 'recreateGridMenu'); + jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); + + control.initEventHandlers(); + gridStub.onSetOptions.notify({ grid: gridStub, optionsBefore: { frozenColumn: -1 }, optionsAfter: { frozenColumn: 2 } }, new Slick.EventData(), gridStub); + expect(recreateSpy).toHaveBeenCalledTimes(1); + + gridStub.onSetOptions.notify({ grid: gridStub, optionsBefore: { frozenColumn: 2 }, optionsAfter: { frozenColumn: -1 } }, new Slick.EventData(), gridStub); + expect(recreateSpy).toHaveBeenCalledTimes(2); + }); + + it('should query an input checkbox change event and expect "headerColumnValueExtractor" method to be called when defined', () => { + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); + + gridOptionsMock.gridMenu.headerColumnValueExtractor = (column: Column) => `${column?.columnGroup || ''} - ${column.name}`; + control.columns = columnsMock; + gridOptionsMock.frozenColumn = 0; + control.initEventHandlers(); + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); + const liElmList = control.menuElement.querySelectorAll('li'); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(readjustSpy).toHaveBeenCalledWith(0, columnsMock, columnsMock); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + expect(liElmList[2].textContent).toBe('Billing - Field 3'); + }); + + it('should query an input checkbox change event and expect "headerColumnValueExtractor" method to be called from default option when it is not provided', () => { + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); + + gridOptionsMock.gridMenu.headerColumnValueExtractor = null; + control.columns = columnsMock; + gridOptionsMock.frozenColumn = 0; + control.initEventHandlers(); + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); + const liElmList = control.menuElement.querySelectorAll('li'); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(readjustSpy).toHaveBeenCalledWith(0, columnsMock, columnsMock); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + expect(liElmList[2].textContent).toBe('Field 3'); + }); + + it('should open the Grid Menu and expect its minWidth and height to be overriden when provided as grid menu options', () => { + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + + gridOptionsMock.gridMenu.contentMinWidth = 200; + gridOptionsMock.gridMenu.height = 300; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const gridMenuElm = document.querySelector('.slick-gridmenu') as HTMLDivElement; + + expect(gridMenuElm.style.minWidth).toBe('200px'); + expect(gridMenuElm.style.height).toBe('300px'); + }); + + it('should open the Grid Menu via "showGridMenu" method from an external button which has span inside it and expect the Grid Menu still work', () => { + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + + control.init(); + const spanEvent = new MouseEvent('click', { bubbles: true, cancelable: true, composed: false }) + const spanBtnElm = document.createElement('span'); + const buttonElm = document.createElement('button'); + spanBtnElm.textContent = 'Grid Menu'; + Object.defineProperty(spanEvent, 'target', { writable: true, configurable: true, value: spanBtnElm }); + Object.defineProperty(spanBtnElm, 'parentElement', { writable: true, configurable: true, value: buttonElm }); + control.showGridMenu(spanEvent, { alignDropSide: 'left' }); + const gridMenuElm = document.querySelector('.slick-gridmenu') as HTMLDivElement; + + expect(gridMenuElm.style.visibility).toBe('visible'); + }); + + it('should open the Grid Menu and expect "Forcefit" to be checked when "hideForceFitButton" is false', () => { + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + + gridOptionsMock.gridMenu.hideForceFitButton = false; + gridOptionsMock.forceFitColumns = true; + control.columns = columnsMock; + control.initEventHandlers(); + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); + const inputForcefitElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-forcefit'); + const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-gridmenu-colpicker-forcefit]'); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + expect(inputForcefitElm.checked).toBeTruthy(); + expect(inputForcefitElm.dataset.option).toBe('autoresize') + expect(labelSyncElm.textContent).toBe('Force fit columns'); + }); + + it('should open the Grid Menu and expect "Sync Resize" to be checked when "hideSyncResizeButton" is false', () => { + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + + gridOptionsMock.gridMenu.hideSyncResizeButton = false; + gridOptionsMock.syncColumnCellResize = true; + control.columns = columnsMock; + control.initEventHandlers(); + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); + const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-syncresize'); + const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-gridmenu-colpicker-syncresize]'); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + expect(inputSyncElm.checked).toBeTruthy(); + expect(inputSyncElm.dataset.option).toBe('syncresize'); + expect(labelSyncElm.textContent).toBe('Synchronous resize'); + }); + + it('should open the Grid Menu and expect "onColumnsChanged" to be called when defined', () => { + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); + const onColChangedMock = jest.fn(); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + + gridOptionsMock.gridMenu.onColumnsChanged = onColChangedMock; + control.columns = columnsMock; + control.initEventHandlers(); + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); + + const expectedCallbackArgs = { + columnId: 'field1', + showing: true, + allColumns: columnsMock, + columns: columnsMock, + visibleColumns: columnsMock, + grid: gridStub, + }; + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + expect(onColChangedMock).toBeCalledWith(expect.anything(), expectedCallbackArgs); + expect(pubSubSpy).toHaveBeenCalledWith('gridMenu:onColumnsChanged', expectedCallbackArgs); + }); + + it('should open the grid menu via its hamburger menu and click on "Force Fit Columns" checkbox and expect "setOptions" and "setColumns" to be called with previous visible columns', () => { + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + jest.spyOn(control, 'getVisibleColumns').mockReturnValue(columnsMock.slice(1)); + const setOptionSpy = jest.spyOn(gridStub, 'setOptions'); + const setColumnSpy = jest.spyOn(gridStub, 'setColumns'); + + gridOptionsMock.gridMenu.hideForceFitButton = false; + gridOptionsMock.gridMenu.forceFitTitle = 'Custom Force Fit'; + control.columns = columnsMock; + control.initEventHandlers(); + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const inputForcefitElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-forcefit'); + const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-gridmenu-colpicker-forcefit]'); + inputForcefitElm.dispatchEvent(new Event('click', { bubbles: true })); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(inputForcefitElm.checked).toBeTruthy(); + expect(inputForcefitElm.dataset.option).toBe('autoresize'); + expect(labelSyncElm.textContent).toBe('Custom Force Fit'); + expect(setOptionSpy).toHaveBeenCalledWith({ forceFitColumns: true }); + expect(setColumnSpy).toHaveBeenCalledWith(columnsMock.slice(1)); + }); + + it('should open the grid menu via its hamburger menu and click on "syncresize" checkbox and expect "setOptions" to be called with "syncColumnCellResize" property', () => { + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + jest.spyOn(control, 'getVisibleColumns').mockReturnValue(columnsMock.slice(1)); + const setOptionSpy = jest.spyOn(gridStub, 'setOptions'); + + gridOptionsMock.gridMenu.hideSyncResizeButton = false; + gridOptionsMock.gridMenu.syncResizeTitle = 'Custom Resize Title'; + gridOptionsMock.syncColumnCellResize = true; + control.columns = columnsMock; + control.initEventHandlers(); + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-syncresize'); + const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-gridmenu-colpicker-syncresize]'); + inputSyncElm.dispatchEvent(new Event('click', { bubbles: true })); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(inputSyncElm.checked).toBeTruthy(); + expect(inputSyncElm.dataset.option).toBe('syncresize'); + expect(labelSyncElm.textContent).toBe('Custom Resize Title'); + expect(setOptionSpy).toHaveBeenCalledWith({ syncColumnCellResize: true }); + }); + + it('should NOT show the Grid Menu when user defines the callback "menuUsabilityOverride" which returns False', () => { + gridOptionsMock.gridMenu.menuUsabilityOverride = () => false; + gridOptionsMock.gridMenu.hideForceFitButton = false; + gridOptionsMock.gridMenu.hideSyncResizeButton = false; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const forceFitElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-forcefit'); + const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-syncresize'); + + expect(control.menuElement.style.visibility).toBe('hidden'); + expect(forceFitElm).toBeFalsy(); + expect(inputSyncElm).toBeFalsy(); + }); + + it('should NOT show the Grid Menu when user defines the callback "onBeforeMenuShow" which returns False', () => { + const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); + gridOptionsMock.gridMenu.onBeforeMenuShow = () => false; + gridOptionsMock.gridMenu.hideForceFitButton = false; + gridOptionsMock.gridMenu.hideSyncResizeButton = false; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const forceFitElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-forcefit'); + const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-syncresize'); + + expect(control.menuElement.style.visibility).toBe('hidden'); + expect(forceFitElm).toBeFalsy(); + expect(inputSyncElm).toBeFalsy(); + expect(pubSubSpy).toHaveBeenCalledWith('gridMenu:onBeforeMenuShow', { + grid: gridStub, + menu: document.querySelector('.slick-gridmenu'), + allColumns: columnsMock, + visibleColumns: columnsMock + }); + }); + + it('should show the Grid Menu when user defines the callback "onBeforeMenuShow" which returns True', () => { + gridOptionsMock.gridMenu.onBeforeMenuShow = () => true; + gridOptionsMock.gridMenu.hideForceFitButton = false; + gridOptionsMock.gridMenu.hideSyncResizeButton = false; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const forceFitElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-forcefit'); + const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-syncresize'); + + expect(control.menuElement.style.visibility).toBe('visible'); + expect(forceFitElm).toBeTruthy(); + expect(inputSyncElm).toBeTruthy(); + }); + + it('should execute "onAfterMenuShow" callback when defined', () => { + const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); + gridOptionsMock.gridMenu.onAfterMenuShow = () => true; + const onAfterSpy = jest.spyOn(gridOptionsMock.gridMenu, 'onAfterMenuShow'); + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + + expect(onAfterSpy).toHaveBeenCalled(); + expect(control.menuElement.style.visibility).toBe('visible'); + + control.hideMenu(new Event('click', { bubbles: true, cancelable: true, composed: false }) as DOMEvent); + expect(control.menuElement.style.visibility).toBe('hidden'); + expect(pubSubSpy).toHaveBeenCalledWith('gridMenu:onAfterMenuShow', { + grid: gridStub, + menu: document.querySelector('.slick-gridmenu'), + allColumns: columnsMock, + visibleColumns: columnsMock + }); + }); + + it('should NOT close the Grid Menu by calling "hideMenu" when user defines the callback "onMenuClose" which returns False', () => { + const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); + + gridOptionsMock.gridMenu.onMenuClose = () => false; + gridOptionsMock.gridMenu.hideForceFitButton = false; + gridOptionsMock.gridMenu.hideSyncResizeButton = false; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const forceFitElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-forcefit'); + const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-syncresize'); + + expect(control.menuElement.style.visibility).toBe('visible'); + expect(forceFitElm).toBeTruthy(); + expect(inputSyncElm).toBeTruthy(); + + control.hideMenu(new Event('click', { bubbles: true, cancelable: true, composed: false }) as DOMEvent); + expect(control.menuElement.style.visibility).toBe('visible'); + expect(pubSubSpy).toHaveBeenCalledWith('gridMenu:onMenuClose', { + grid: gridStub, + menu: document.querySelector('.slick-gridmenu'), + allColumns: columnsMock, + visibleColumns: columnsMock + }); + }); + + it('should close the Grid Menu by calling "hideMenu" when user defines the callback "onMenuClose" which returns True', () => { + const autosizeSpy = jest.spyOn(gridStub, 'autosizeColumns'); + + gridOptionsMock.gridMenu.onMenuClose = () => true; + gridOptionsMock.gridMenu.hideForceFitButton = false; + gridOptionsMock.gridMenu.hideSyncResizeButton = false; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const forceFitElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-forcefit'); + const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-syncresize'); + + expect(control.menuElement.style.visibility).toBe('visible'); + expect(forceFitElm).toBeTruthy(); + expect(inputSyncElm).toBeTruthy(); + + control.hideMenu(new Event('click', { bubbles: true, cancelable: true, composed: false }) as DOMEvent); + expect(control.menuElement.style.visibility).toBe('hidden'); + expect(autosizeSpy).not.toHaveBeenCalled(); + }); + + it('should close the Grid Menu by calling "hideMenu" and call "autosizeColumns" when "enableAutoSizeColumns" is enabled and the columns are different', () => { + gridOptionsMock.gridMenu.hideForceFitButton = false; + gridOptionsMock.gridMenu.hideSyncResizeButton = false; + gridOptionsMock.enableAutoSizeColumns = true; + const autosizeSpy = jest.spyOn(gridStub, 'autosizeColumns'); + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const forceFitElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-forcefit'); + const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-gridmenu-colpicker-syncresize'); + const pickerField1Elm = document.querySelector('input[type="checkbox"][data-columnid="field1"]') as HTMLInputElement; + expect(pickerField1Elm.checked).toBeTrue(); + pickerField1Elm.checked = false; + pickerField1Elm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + + expect(control.menuElement.style.visibility).toBe('visible'); + expect(forceFitElm).toBeTruthy(); + expect(inputSyncElm).toBeTruthy(); + expect(pickerField1Elm.checked).toBeFalse(); + + control.hideMenu(new Event('click', { bubbles: true, cancelable: true, composed: false }) as DOMEvent); + expect(control.menuElement.style.visibility).toBe('hidden'); + expect(autosizeSpy).toHaveBeenCalled(); + }); + + it('should add a custom Grid Menu item and expect the "action" and "onCommand" callbacks to be called when command is clicked in the list', () => { + const helpFnMock = jest.fn(); + const onCommandMock = jest.fn(); + const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); + + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', action: helpFnMock }]; + gridOptionsMock.gridMenu.onCommand = onCommandMock; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + helpCommandElm.dispatchEvent(clickEvent); + + const expectedCallbackArgs = { + grid: gridStub, + command: 'help', + item: { command: 'help', title: 'Help', action: helpFnMock }, + allColumns: columnsMock, + visibleColumns: columnsMock + }; + expect(helpFnMock).toHaveBeenCalled(); + expect(onCommandMock).toHaveBeenCalledWith(clickEvent, expectedCallbackArgs); + expect(pubSubSpy).toHaveBeenCalledWith('gridMenu:onCommand', expectedCallbackArgs); + }); + + it('should add a custom Grid Menu item and NOT expect the "action" and "onCommand" callbacks to be called when item is "disabled"', () => { + const helpFnMock = jest.fn(); + const onCommandMock = jest.fn(); + + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', action: helpFnMock, disabled: true }]; + gridOptionsMock.gridMenu.onCommand = onCommandMock; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + helpCommandElm.dispatchEvent(clickEvent); + + expect(helpFnMock).not.toHaveBeenCalled(); + expect(onCommandMock).not.toHaveBeenCalled(); + expect(helpCommandElm.classList.contains('slick-gridmenu-item-disabled')).toBeTrue(); + }); + + it('should add a custom Grid Menu item and NOT expect the "action" and "onCommand" callbacks to be called when item "itemUsabilityOverride" callback returns False', () => { + const helpFnMock = jest.fn(); + const onCommandMock = jest.fn(); + + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', action: helpFnMock, itemUsabilityOverride: () => false }]; + gridOptionsMock.gridMenu.onCommand = onCommandMock; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + helpCommandElm.dispatchEvent(clickEvent); + + expect(helpFnMock).not.toHaveBeenCalled(); + expect(onCommandMock).not.toHaveBeenCalled(); + }); + + it('should add a custom Grid Menu item and expect the "action" and "onCommand" callbacks to be called when command is clicked in the list and its "itemUsabilityOverride" callback returns True', () => { + const helpFnMock = jest.fn(); + const onCommandMock = jest.fn(); + + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', action: helpFnMock, itemUsabilityOverride: () => true }]; + gridOptionsMock.gridMenu.onCommand = onCommandMock; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + helpCommandElm.dispatchEvent(clickEvent); + + expect(helpFnMock).toHaveBeenCalled(); + expect(onCommandMock).toHaveBeenCalledWith(clickEvent, { + grid: gridStub, + command: 'help', + item: { command: 'help', title: 'Help', action: helpFnMock, disabled: false, itemUsabilityOverride: expect.toBeFunction(), }, + allColumns: columnsMock, + visibleColumns: columnsMock + }); + }); + + it('should add a custom Grid Menu item and expect item to be hidden from the DOM list when "hidden" is enabled', () => { + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', hidden: true }]; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + + expect(helpCommandElm.classList.contains('slick-gridmenu-item-hidden')).toBeTrue(); + }); + + it('should add a custom Grid Menu item and expect item to NOT be created in the DOM list when "itemVisibilityOverride" callback returns False', () => { + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', itemVisibilityOverride: () => false }]; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + + expect(helpCommandElm).toBeFalsy(); + }); + + it('should add a custom Grid Menu item and expect item to be disabled when "disabled" is set to True', () => { + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', disabled: true }]; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + + expect(helpCommandElm.classList.contains('slick-gridmenu-item-disabled')).toBeTrue(); + }); + + it('should add a custom Grid Menu "divider" item object and expect a divider to be created', () => { + gridOptionsMock.gridMenu.customItems = [{ command: 'divider', divider: true }]; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=divider]'); + + expect(helpCommandElm.classList.contains('slick-gridmenu-item-divider')).toBeTrue(); + }); + + it('should add a custom Grid Menu "divider" string and expect a divider to be created', () => { + gridOptionsMock.gridMenu.customItems = ['divider']; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item'); + + expect(helpCommandElm.classList.contains('slick-gridmenu-item-divider')).toBeTrue(); + }); + + it('should add a custom Grid Menu item with "cssClass" and expect all classes to be added to the item in the DOM', () => { + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', cssClass: 'text-danger red' }]; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + + expect(helpCommandElm.classList.contains('slick-gridmenu-item')).toBeTrue(); + expect(helpCommandElm.classList.contains('text-danger')).toBeTrue(); + expect(helpCommandElm.classList.contains('red')).toBeTrue(); + expect(helpCommandElm.className).toBe('slick-gridmenu-item text-danger red'); + }); + + it('should add a custom Grid Menu item with "iconCssClass" and expect an icon to be included on the item DOM element', () => { + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', iconCssClass: 'mdi mdi-close' }]; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + const helpIconElm = helpCommandElm.querySelector('.slick-gridmenu-icon'); + const helpTextElm = helpCommandElm.querySelector('.slick-gridmenu-content'); + + expect(helpTextElm.textContent).toBe('Help'); + expect(helpIconElm.classList.contains('slick-gridmenu-icon')).toBeTrue(); + expect(helpIconElm.classList.contains('mdi')).toBeTrue(); + expect(helpIconElm.classList.contains('mdi-close')).toBeTrue(); + expect(helpIconElm.className).toBe('slick-gridmenu-icon mdi mdi-close'); + }); + + it('should add a custom Grid Menu item with "iconImage" and expect an icon to be included on the item DOM element', () => { + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', iconImage: '/images/some-image.png' }]; + gridOptionsMock.gridMenu.iconCssClass = undefined; + gridOptionsMock.gridMenu.iconImage = '/images/some-gridmenu-image.png'; + + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button') as HTMLButtonElement; + const buttonImageElm = buttonElm.querySelector('img') as HTMLImageElement; + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + const helpIconElm = helpCommandElm.querySelector('.slick-gridmenu-icon'); + const helpTextElm = helpCommandElm.querySelector('.slick-gridmenu-content'); + + expect(buttonImageElm.src).toBe('/images/some-gridmenu-image.png'); + expect(helpTextElm.textContent).toBe('Help'); + expect(helpIconElm.style.backgroundImage).toBe('url(/images/some-image.png)') + expect(consoleWarnSpy).toHaveBeenCalledWith('[Slickgrid-Universal] The "iconImage" property of a Grid Menu item is no deprecated and will be removed in future version, consider using "iconCssClass" instead.'); + }); + + it('should add a custom Grid Menu item with "tooltip" and expect the item title attribute to be part of the item DOM element', () => { + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', tooltip: 'some tooltip text' }]; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + + expect(helpCommandElm.title).toBe('some tooltip text'); + }); + + it('should add a custom Grid Menu item with "textCssClass" and expect extra css classes added to the item text DOM element', () => { + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', textCssClass: 'red bold' }]; + control.columns = columnsMock; + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + const helpTextElm = helpCommandElm.querySelector('.slick-gridmenu-content'); + + expect(helpTextElm.textContent).toBe('Help'); + expect(helpTextElm.classList.contains('red')).toBeTrue(); + expect(helpTextElm.classList.contains('bold')).toBeTrue(); + expect(helpTextElm.className).toBe('slick-gridmenu-content red bold'); + }); + + it('should add a custom Grid Menu item and provide a custom title for the custom items list', () => { + gridOptionsMock.gridMenu.customItems = [{ command: 'help', title: 'Help', textCssClass: 'red bold' }]; + control.columns = columnsMock; + control.init(); + gridOptionsMock.gridMenu.customTitle = 'Custom Title'; + control.updateAllTitles(gridOptionsMock.gridMenu); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + const customTitleElm = control.menuElement.querySelector('.slick-gridmenu-custom .title'); + const helpCommandElm = control.menuElement.querySelector('.slick-gridmenu-item[data-command=help]'); + const helpTextElm = helpCommandElm.querySelector('.slick-gridmenu-content'); + + expect(customTitleElm.textContent).toBe('Custom Title'); + expect(helpTextElm.textContent).toBe('Help'); + expect(helpTextElm.classList.contains('red')).toBeTrue(); + expect(helpTextElm.classList.contains('bold')).toBeTrue(); + expect(helpTextElm.className).toBe('slick-gridmenu-content red bold'); + }); + + it('should be able to recreate the Grid Menu', () => { + const deleteSpy = jest.spyOn(control, 'deleteMenu'); + const initSpy = jest.spyOn(control, 'init'); + + control.recreateGridMenu(); + + expect(deleteSpy).toBeCalled(); + expect(initSpy).toBeCalled(); + }); + + describe('addGridMenuCustomCommands method', () => { + afterEach(() => { + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + }); + + it('should expect an empty "customItems" array when both Filter & Sort are disabled', () => { + control.columns = columnsMock; + control.init(); + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([]); + }); + + it('should expect menu related to "Unfreeze Columns/Rows"', () => { + const copyGridOptionsMock = { ...gridOptionsMock, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: false, } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + { iconCssClass: 'fa fa-times', title: 'Dégeler les colonnes/rangées', disabled: false, command: 'clear-pinning', positionOrder: 52 }, + ]); + }); + + it('should expect all menu related to Filter when "enableFilering" is set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: true, } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + { iconCssClass: 'fa fa-filter text-danger', title: 'Supprimer tous les filtres', disabled: false, command: 'clear-filter', positionOrder: 50 }, + { iconCssClass: 'fa fa-random', title: 'Basculer la ligne des filtres', disabled: false, command: 'toggle-filter', positionOrder: 53 }, + { iconCssClass: 'fa fa-refresh', title: 'Rafraîchir les données', disabled: false, command: 'refresh-dataset', positionOrder: 57 } + ]); + }); + + it('should have only 1 menu "clear-filter" when all other menus are defined as hidden & when "enableFilering" is set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideToggleFilterCommand: true, hideRefreshDatasetCommand: true } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + { iconCssClass: 'fa fa-filter text-danger', title: 'Supprimer tous les filtres', disabled: false, command: 'clear-filter', positionOrder: 50 } + ]); + }); + + it('should have only 1 menu "toggle-filter" when all other menus are defined as hidden & when "enableFilering" is set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideClearAllFiltersCommand: true, hideRefreshDatasetCommand: true } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + { iconCssClass: 'fa fa-random', title: 'Basculer la ligne des filtres', disabled: false, command: 'toggle-filter', positionOrder: 53 }, + ]); + }); + + it('should have only 1 menu "refresh-dataset" when all other menus are defined as hidden & when "enableFilering" is set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideClearAllFiltersCommand: true, hideToggleFilterCommand: true } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + { iconCssClass: 'fa fa-refresh', title: 'Rafraîchir les données', disabled: false, command: 'refresh-dataset', positionOrder: 57 } + ]); + }); + + it('should have the "toggle-preheader" menu command when "showPreHeaderPanel" is set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, showPreHeaderPanel: true } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + { iconCssClass: 'fa fa-random', title: 'Basculer la ligne de pré-en-tête', disabled: false, command: 'toggle-preheader', positionOrder: 53 } + ]); + }); + + it('should not have the "toggle-preheader" menu command when "showPreHeaderPanel" and "hideTogglePreHeaderCommand" are set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, showPreHeaderPanel: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideTogglePreHeaderCommand: true } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([]); + }); + + it('should have the "clear-sorting" menu command when "enableSorting" is set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableSorting: true } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + { iconCssClass: 'fa fa-unsorted text-danger', title: 'Supprimer tous les tris', disabled: false, command: 'clear-sorting', positionOrder: 51 } + ]); + }); + + it('should not have the "clear-sorting" menu command when "enableSorting" and "hideClearAllSortingCommand" are set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableSorting: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideClearAllSortingCommand: true } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([]); + }); + + it('should have the "export-csv" menu command when "enableTextExport" is set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideExportExcelCommand: true, hideExportTextDelimitedCommand: true } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + { iconCssClass: 'fa fa-download', title: 'Exporter en format CSV', disabled: false, command: 'export-csv', positionOrder: 54 } + ]); + }); + + it('should not have the "export-csv" menu command when "enableTextExport" and "hideExportCsvCommand" are set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideExportExcelCommand: true, hideExportCsvCommand: true, hideExportTextDelimitedCommand: true } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([]); + }); + + it('should have the "export-excel" menu command when "enableTextExport" is set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: true, enableTextExport: false, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideExportCsvCommand: true, hideExportExcelCommand: false } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + { iconCssClass: 'fa fa-file-excel-o text-success', title: 'Exporter vers Excel', disabled: false, command: 'export-excel', positionOrder: 55 } + ]); + }); + + it('should have the "export-text-delimited" menu command when "enableTextExport" is set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideExportCsvCommand: true, hideExportExcelCommand: true } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + { iconCssClass: 'fa fa-download', title: 'Exporter en format texte (délimité par tabulation)', disabled: false, command: 'export-text-delimited', positionOrder: 56 } + ]); + }); + + it('should not have the "export-text-delimited" menu command when "enableTextExport" and "hideExportCsvCommand" are set', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideExportExcelCommand: true, hideExportCsvCommand: true, hideExportTextDelimitedCommand: true } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.columns = columnsMock; + control.init(); + control.init(); // calling 2x register to make sure it doesn't duplicate commands + expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([]); + }); + }); + + // describe('adding Grid Menu Custom Items', () => { + // const customItemsMock = [{ + // iconCssClass: 'fa fa-question-circle', + // titleKey: 'HELP', + // disabled: false, + // command: 'help', + // positionOrder: 99 + // }]; + + // beforeEach(() => { + // const copyGridOptionsMock = { + // ...gridOptionsMock, + // enableTextExport: true, + // gridMenu: { + // commandLabels: gridOptionsMock.gridMenu.commandLabels, + // customItems: customItemsMock, + // hideClearFrozenColumnsCommand: true, + // hideExportCsvCommand: false, + // hideExportExcelCommand: false, + // hideExportTextDelimitedCommand: true, + // hideRefreshDatasetCommand: true, + // hideSyncResizeButton: true, + // hideToggleFilterCommand: true, + // hideTogglePreHeaderCommand: true + // } + // } as unknown as GridOption; + + // jest.spyOn(SharedService.prototype, 'dataView', 'get').mockReturnValue(dataViewStub); + // jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); + // jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + // jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(columnsMock); + // jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(columnsMock); + // jest.spyOn(SharedService.prototype, 'columnDefinitions', 'get').mockReturnValue(columnsMock); + // jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + // }); + + // afterEach(() => { + // control.dispose(); + // }); + + // xit('should have user grid menu custom items', () => { + // control.register(); + // expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + // { command: 'export-csv', disabled: false, iconCssClass: 'fa fa-download', positionOrder: 54, title: 'Exporter en format CSV' }, + // // { command: 'export-excel', disabled: false, iconCssClass: 'fa fa-file-excel-o text-success', positionOrder: 54, title: 'Exporter vers Excel' }, + // { command: 'help', disabled: false, iconCssClass: 'fa fa-question-circle', positionOrder: 99, title: 'Aide', titleKey: 'HELP' }, + // ]); + // }); + + // xit('should have same user grid menu custom items even when grid menu extension is registered multiple times', () => { + // control.register(); + // control.register(); + // expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ + // { command: 'export-csv', disabled: false, iconCssClass: 'fa fa-download', positionOrder: 54, title: 'Exporter en format CSV' }, + // // { command: 'export-excel', disabled: false, iconCssClass: 'fa fa-file-excel-o text-success', positionOrder: 54, title: 'Exporter vers Excel' }, + // { command: 'help', disabled: false, iconCssClass: 'fa fa-question-circle', positionOrder: 99, title: 'Aide', titleKey: 'HELP' }, + // ]); + // }); + // }); + + // describe('refreshBackendDataset method', () => { + // afterEach(() => { + // jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + // }); + + // xit('should throw an error when backendServiceApi is not provided in the grid options', () => { + // const copyGridOptionsMock = { ...gridOptionsMock, backendServiceApi: {} } as unknown as GridOption; + // jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + // expect(() => control.refreshBackendDataset()).toThrowError(`BackendServiceApi requires at least a "process" function and a "service" defined`); + // }); + + // xit('should call the backend service API to refresh the dataset', (done) => { + // const now = new Date(); + // const query = `query { users (first:20,offset:0) { totalCount, nodes { id,name,gender,company } } }`; + // const processResult = { + // data: { users: { nodes: [] }, pageInfo: { hasNextPage: true }, totalCount: 0 }, + // metrics: { startTime: now, endTime: now, executionTime: 0, totalItemCount: 0 } + // }; + // const preSpy = jest.spyOn(gridOptionsMock.backendServiceApi as BackendServiceApi, 'preProcess'); + // const postSpy = jest.spyOn(gridOptionsMock.backendServiceApi as BackendServiceApi, 'postProcess'); + // const promise = new Promise((resolve) => setTimeout(() => resolve(processResult), 1)); + // const processSpy = jest.spyOn(gridOptionsMock.backendServiceApi as BackendServiceApi, 'process').mockReturnValue(promise); + // jest.spyOn(gridOptionsMock.backendServiceApi!.service, 'buildQuery').mockReturnValue(query); + + // control.refreshBackendDataset({ enableAddRow: true }); + + // expect(preSpy).toHaveBeenCalled(); + // expect(processSpy).toHaveBeenCalled(); + // promise.then(() => { + // expect(postSpy).toHaveBeenCalledWith(processResult); + // done(); + // }); + // }); + // }); + + describe('executeGridMenuInternalCustomCommands method', () => { + beforeEach(() => { + jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); + jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(columnsMock); + jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(columnsMock.slice(0, 1)); + }); + + // afterEach(() => { + // jest.clearAllMocks(); + // control.eventHandler.unsubscribeAll(); + // mockGridMenuAddon.onCommand = new Slick.Event(); + // }); + + it('should call "clearFrozenColumns" when the command triggered is "clear-pinning"', () => { + // const onCommandMock = jest.fn(); + // gridOptionsMock.gridMenu.onCommand = onCommandMock; + const setOptionsSpy = jest.spyOn(gridStub, 'setOptions'); + const setColumnsSpy = jest.spyOn(gridStub, 'setColumns'); + const copyGridOptionsMock = { ...gridOptionsMock, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: false, } } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + + control.init(); + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=clear-pinning]').dispatchEvent(clickEvent); + + // expect(onCommandMock).toHaveBeenCalled(); + expect(setColumnsSpy).toHaveBeenCalled(); + expect(setOptionsSpy).toHaveBeenCalledWith({ frozenColumn: -1, frozenRow: -1, frozenBottom: false, enableMouseWheelScrollHandler: false }); + }); + + it('should call "clearFilters" and dataview refresh when the command triggered is "clear-filter"', () => { + const filterSpy = jest.spyOn(filterServiceStub, 'clearFilters'); + const refreshSpy = jest.spyOn(SharedService.prototype.dataView, 'refresh'); + const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: true, } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + + control.init(); + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=clear-filter]').dispatchEvent(clickEvent); + + expect(filterSpy).toHaveBeenCalled(); + expect(refreshSpy).toHaveBeenCalled(); + }); + + it('should call "clearSorting" and dataview refresh when the command triggered is "clear-sorting"', () => { + const sortSpy = jest.spyOn(sortServiceStub, 'clearSorting'); + const refreshSpy = jest.spyOn(SharedService.prototype.dataView, 'refresh'); + const copyGridOptionsMock = { ...gridOptionsMock, enableSorting: true, } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + + control.init(); + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=clear-sorting]').dispatchEvent(clickEvent); + + expect(sortSpy).toHaveBeenCalled(); + expect(refreshSpy).toHaveBeenCalled(); + }); + + it('should call "exportToExcel" and expect an error thrown when ExcelExportService is not registered prior to calling the method', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: true, } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([]); + + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=export-excel]').dispatchEvent(clickEvent); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.toInclude('[Slickgrid-Universal] You must register the ExcelExportService to properly use Export to Excel in the Grid Menu.')); + }); + + it('should call "exportToFile" with CSV and expect an error thrown when TextExportService is not registered prior to calling the method', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, hideExportCsvCommand: false, } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([]); + + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=export-csv]').dispatchEvent(clickEvent); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.toInclude('[Slickgrid-Universal] You must register the TextExportService to properly use Export to File in the Grid Menu.')); + }); + + it('should call "exportToFile" with Text Delimited and expect an error thrown when TextExportService is not registered prior to calling the method', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, hideExportTextDelimitedCommand: false, } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([]); + + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=export-text-delimited]').dispatchEvent(clickEvent); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.toInclude('[Slickgrid-Universal] You must register the TextExportService to properly use Export to File in the Grid Menu.')); + }); + + it('should call "exportToExcel" when the command triggered is "export-excel"', () => { + const excelExportSpy = jest.spyOn(excelExportServiceStub, 'exportToExcel'); + const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: true, } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([excelExportServiceStub]); + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + + control.init(); + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=export-excel]').dispatchEvent(clickEvent); + + expect(excelExportSpy).toHaveBeenCalled(); + }); + + it('should call "exportToFile" with CSV set when the command triggered is "export-csv"', () => { + const exportSpy = jest.spyOn(textExportServiceStub, 'exportToFile'); + const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([textExportServiceStub]); + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + + control.init(); + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=export-csv]').dispatchEvent(clickEvent); + + expect(exportSpy).toHaveBeenCalledWith({ delimiter: DelimiterType.comma, format: FileType.csv }); + }); + + it('should call "exportToFile" with Text Delimited set when the command triggered is "export-text-delimited"', () => { + const exportSpy = jest.spyOn(textExportServiceStub, 'exportToFile'); + const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, hideExportTextDelimitedCommand: false } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([textExportServiceStub]); + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + + control.init(); + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=export-text-delimited]').dispatchEvent(clickEvent); + + expect(exportSpy).toHaveBeenCalledWith({ delimiter: DelimiterType.tab, format: FileType.txt }); + }); + + it('should call the grid "setHeaderRowVisibility" method when the command triggered is "toggle-filter"', () => { + let copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: false, hideToggleFilterCommand: false } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + const setHeaderSpy = jest.spyOn(gridStub, 'setHeaderRowVisibility'); + const scrollSpy = jest.spyOn(gridStub, 'scrollColumnIntoView'); + const setColumnSpy = jest.spyOn(gridStub, 'setColumns'); + + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=toggle-filter]').dispatchEvent(clickEvent); + + expect(setHeaderSpy).toHaveBeenCalledWith(true); + expect(scrollSpy).toHaveBeenCalledWith(0); + expect(setColumnSpy).toHaveBeenCalledTimes(1); + + copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: true, hideToggleFilterCommand: false } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=toggle-filter]').dispatchEvent(clickEvent); + + expect(setHeaderSpy).toHaveBeenCalledWith(false); + expect(setColumnSpy).toHaveBeenCalledTimes(1); // same as before, so count won't increase + }); + + it('should call the grid "setPreHeaderPanelVisibility" method when the command triggered is "toggle-preheader"', () => { + let copyGridOptionsMock = { ...gridOptionsMock, showPreHeaderPanel: true, hideTogglePreHeaderCommand: false } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + const gridSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setPreHeaderPanelVisibility'); + + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=toggle-preheader]').dispatchEvent(clickEvent); + + expect(gridSpy).toHaveBeenCalledWith(false); + + copyGridOptionsMock = { ...gridOptionsMock, showPreHeaderPanel: false, hideTogglePreHeaderCommand: false } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=toggle-preheader]').dispatchEvent(clickEvent); + + expect(gridSpy).toHaveBeenCalledWith(true); + }); + + it('should call "refreshBackendDataset" method when the command triggered is "refresh-dataset"', () => { + const refreshSpy = jest.spyOn(extensionUtility, 'refreshBackendDataset'); + const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, hideHeaderRowAfterPageLoad: false, hideRefreshDatasetCommand: false, } as unknown as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + + control.init(); + control.columns = columnsMock; + const clickEvent = new Event('click', { bubbles: true, cancelable: true, composed: false }); + document.querySelector('.slick-gridmenu-button').dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('.slick-gridmenu-item[data-command=refresh-dataset]').dispatchEvent(clickEvent); + + expect(refreshSpy).toHaveBeenCalled(); + }); + }); + }); + + describe('onColumnsReordered event', () => { + it('should reorder some columns', () => { + const columnsUnorderedMock: Column[] = [ + { id: 'field2', field: 'field2', name: 'Field 2', width: 75 }, + { id: 'field1', field: 'field1', name: 'Field 1', width: 100, nameKey: 'TITLE' }, + { id: 'field3', field: 'field3', name: 'Field 3', width: 75, columnGroup: 'Billing' }, + ]; + const columnsMock: Column[] = [ + { id: 'field1', field: 'field1', name: 'Field 1', width: 100, nameKey: 'TITLE' }, + { id: 'field2', field: 'field2', name: 'Field 2', width: 75 }, + { id: 'field3', field: 'field3', name: 'Field 3', width: 75, columnGroup: 'Billing' }, + ]; + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValueOnce(0).mockReturnValueOnce(1); + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + + control.columns = columnsUnorderedMock; + control.initEventHandlers(); + control.init(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + gridStub.onColumnsReordered.notify({ impactedColumns: columnsUnorderedMock, grid: gridStub }, eventData, gridStub); + control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + expect(control.columns).toEqual(columnsMock); + }); + }); + }); + + describe('translateGridMenu method', () => { + beforeEach(() => { + control.dispose(); + document.body.innerHTML = ''; + div = document.createElement('div'); + div.innerHTML = template; + document.body.appendChild(div); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(columnsMock); + }); + + it('should translate the column picker header titles', () => { + const handlerSpy = jest.spyOn(control.eventHandler, 'subscribe'); + const utilitySpy = jest.spyOn(extensionUtility, 'getPickerTitleOutputString'); + const translateSpy = jest.spyOn(extensionUtility, 'translateItems'); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined).mockReturnValue(1); + + gridOptionsMock.gridMenu.hideForceFitButton = false; + gridOptionsMock.gridMenu.hideSyncResizeButton = false; + gridOptionsMock.syncColumnCellResize = true; + gridOptionsMock.forceFitColumns = true; + control.columns = columnsMock; + control.initEventHandlers(); + control.init(); + control.translateGridMenu(); + const buttonElm = document.querySelector('.slick-gridmenu-button'); + buttonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false })); + control.menuElement.querySelector('input[type="checkbox"]').dispatchEvent(new Event('click', { bubbles: true })); + const labelForcefitElm = control.menuElement.querySelector('label[for=slickgrid_124343-gridmenu-colpicker-forcefit]'); + const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-gridmenu-colpicker-syncresize]'); + + expect(handlerSpy).toHaveBeenCalledTimes(3); + expect(labelForcefitElm.textContent).toBe('Ajustement forcé des colonnes'); + expect(labelSyncElm.textContent).toBe('Redimension synchrone'); + expect(utilitySpy).toHaveBeenCalled(); + expect(translateSpy).toHaveBeenCalled(); + expect((SharedService.prototype.gridOptions.gridMenu as GridMenu).columnTitle).toBe('Colonnes'); + expect((SharedService.prototype.gridOptions.gridMenu as GridMenu).forceFitTitle).toBe('Ajustement forcé des colonnes'); + expect((SharedService.prototype.gridOptions.gridMenu as GridMenu).syncResizeTitle).toBe('Redimension synchrone'); + expect(columnsMock).toEqual([ + { id: 'field1', field: 'field1', name: 'Titre', width: 100, nameKey: 'TITLE' }, + { id: 'field2', field: 'field2', name: 'Field 2', width: 75 }, + { id: 'field3', field: 'field3', name: 'Field 3', columnGroup: 'Billing', width: 75 }, + ]); + expect(control.getAllColumns()).toEqual(columnsMock); + expect(control.getVisibleColumns()).toEqual(columnsMock); + }); + }); +}); \ No newline at end of file diff --git a/packages/common/src/controls/columnPicker.control.ts b/packages/common/src/controls/columnPicker.control.ts index 3eacf6ce8..4a2ac9639 100644 --- a/packages/common/src/controls/columnPicker.control.ts +++ b/packages/common/src/controls/columnPicker.control.ts @@ -9,29 +9,30 @@ import { SlickNamespace, } from '../interfaces/index'; import { ExtensionUtility } from '../extensions/extensionUtility'; -import { emptyElement, SharedService } from '../services'; import { BindingEventService } from '../services/bindingEvent.service'; +import { PubSubService } from '../services/pubSub.service'; +import { SharedService } from '../services/shared.service'; +import { emptyElement, sanitizeTextByAvailableSanitizer } from '../services'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; /** * A control to add a Column Picker (right+click on any column header to reveal the column picker) - * Add the slick.columnpicker.(js|css) files and register it with the grid. * @class ColumnPickerControl * @constructor */ export class ColumnPickerControl { - private _bindEventService: BindingEventService; - private _columns: Column[] = []; - private _columnTitleElm!: HTMLDivElement; - private _eventHandler!: SlickEventHandler; - private _gridUid = ''; - private _listElm!: HTMLSpanElement; - private _menuElm!: HTMLDivElement; - private columnCheckboxes: HTMLInputElement[] = []; - - private _defaults = { + protected _bindEventService: BindingEventService; + protected _columns: Column[] = []; + protected _columnTitleElm!: HTMLDivElement; + protected _eventHandler!: SlickEventHandler; + protected _gridUid = ''; + protected _listElm!: HTMLSpanElement; + protected _menuElm!: HTMLDivElement; + protected columnCheckboxes: HTMLInputElement[] = []; + + protected _defaults = { // the last 2 checkboxes titles hideForceFitButton: false, hideSyncResizeButton: false, @@ -41,11 +42,12 @@ export class ColumnPickerControl { } as ColumnPickerOption; /** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */ - constructor(private readonly extensionUtility: ExtensionUtility, private readonly sharedService: SharedService) { + constructor(protected readonly extensionUtility: ExtensionUtility, protected readonly pubSubService: PubSubService, protected readonly sharedService: SharedService) { this._bindEventService = new BindingEventService(); this._eventHandler = new Slick.EventHandler(); - this._gridUid = ''; this._columns = this.sharedService.allColumns ?? []; + this._gridUid = this.grid?.getUID?.() ?? ''; + this.init(); } @@ -61,11 +63,11 @@ export class ColumnPickerControl { } get controlOptions(): ColumnPickerOption { - return this.sharedService.gridOptions.columnPicker || {}; + return this.gridOptions.columnPicker || {}; } get gridOptions(): GridOption { - return this.grid.getOptions?.() ?? {}; + return this.sharedService.gridOptions ?? {}; } get grid(): SlickGrid { @@ -93,21 +95,21 @@ export class ColumnPickerControl { this._menuElm = document.createElement('div'); this._menuElm.className = `slick-columnpicker ${this._gridUid}`; - this._menuElm.style.display = 'none'; + this._menuElm.style.visibility = 'hidden'; - const gridMenuButtonElm = document.createElement('button'); - gridMenuButtonElm.className = 'close'; - gridMenuButtonElm.type = 'button'; - gridMenuButtonElm.dataset.dismiss = 'slick-columnpicker'; - gridMenuButtonElm.setAttribute('aria-label', 'Close'); + const closePickerButtonElm = document.createElement('button'); + closePickerButtonElm.className = 'close'; + closePickerButtonElm.type = 'button'; + closePickerButtonElm.dataset.dismiss = 'slick-columnpicker'; + closePickerButtonElm.setAttribute('aria-label', 'Close'); const closeSpanElm = document.createElement('span'); closeSpanElm.className = 'close'; closeSpanElm.innerHTML = '×'; closeSpanElm.setAttribute('aria-hidden', 'true'); - gridMenuButtonElm.appendChild(closeSpanElm); - this._menuElm.appendChild(gridMenuButtonElm); + closePickerButtonElm.appendChild(closeSpanElm); + this._menuElm.appendChild(closePickerButtonElm); // user could pass a title on top of the columns list if (this.controlOptions?.columnTitle) { @@ -144,7 +146,7 @@ export class ColumnPickerControl { * @returns {Array} - all columns array */ getAllColumns() { - return this.columns; + return this._columns; } /** @@ -158,7 +160,7 @@ export class ColumnPickerControl { /** Mouse down handler when clicking anywhere in the DOM body */ handleBodyMouseDown(e: DOMEvent) { if ((this._menuElm !== e.target && !this._menuElm.contains(e.target)) || e.target.className === 'close') { - this._menuElm.style.display = 'none'; + this._menuElm.style.visibility = 'hidden'; } } @@ -183,8 +185,8 @@ export class ColumnPickerControl { inputElm = document.createElement('input'); inputElm.type = 'checkbox'; - inputElm.id = `${this._gridUid}colpicker-${columnId}`; - inputElm.dataset.columnId = `${columnId}`; + inputElm.id = `${this._gridUid}-colpicker-${columnId}`; + inputElm.dataset.columnid = `${columnId}`; const colIndex = this.grid.getColumnIndex(columnId); if (colIndex >= 0) { inputElm.checked = true; @@ -199,8 +201,8 @@ export class ColumnPickerControl { } const labelElm = document.createElement('label'); - labelElm.htmlFor = `${this._gridUid}colpicker-${columnId}`; - labelElm.innerHTML = columnLabel; + labelElm.htmlFor = `${this._gridUid}-colpicker-${columnId}`; + labelElm.innerHTML = sanitizeTextByAvailableSanitizer(this.gridOptions, columnLabel); liElm.appendChild(labelElm); } @@ -215,12 +217,12 @@ export class ColumnPickerControl { this._listElm.appendChild(liElm); inputElm = document.createElement('input'); inputElm.type = 'checkbox'; - inputElm.id = `${this._gridUid}colpicker-forcefit`; + inputElm.id = `${this._gridUid}-colpicker-forcefit`; inputElm.dataset.option = 'autoresize'; liElm.appendChild(inputElm); const labelElm = document.createElement('label'); - labelElm.htmlFor = `${this._gridUid}colpicker-forcefit`; + labelElm.htmlFor = `${this._gridUid}-colpicker-forcefit`; labelElm.textContent = `${forceFitTitle ?? ''}`; liElm.appendChild(labelElm); if (this.grid.getOptions().forceFitColumns) { @@ -235,12 +237,12 @@ export class ColumnPickerControl { inputElm = document.createElement('input'); inputElm.type = 'checkbox'; - inputElm.id = `${this._gridUid}colpicker-syncresize`; + inputElm.id = `${this._gridUid}-colpicker-syncresize`; inputElm.dataset.option = 'syncresize'; liElm.appendChild(inputElm); const labelElm = document.createElement('label'); - labelElm.htmlFor = `${this._gridUid}colpicker-syncresize`; + labelElm.htmlFor = `${this._gridUid}-colpicker-syncresize`; labelElm.textContent = `${syncResizeTitle ?? ''}`; liElm.appendChild(labelElm); if (this.grid.getOptions().syncColumnCellResize) { @@ -251,7 +253,7 @@ export class ColumnPickerControl { this._menuElm.style.top = `${(e as any).pageY - 10}px`; this._menuElm.style.left = `${(e as any).pageX - 10}px`; this._menuElm.style.maxHeight = `${document.body.clientHeight - (e as any).pageY - 10}px`; - this._menuElm.style.display = 'block'; + this._menuElm.style.visibility = 'visible'; this._menuElm.appendChild(this._listElm); } @@ -302,7 +304,7 @@ export class ColumnPickerControl { if (e.target.type === 'checkbox') { const isChecked = e.target.checked; - const columnId = e.target.dataset.columnId || ''; + const columnId = e.target.dataset.columnid || ''; const visibleColumns: Column[] = []; this.columnCheckboxes.forEach((columnCheckbox: HTMLInputElement, idx: number) => { if (columnCheckbox.checked) { @@ -327,27 +329,31 @@ export class ColumnPickerControl { // will not have the "selected" CSS class because it wasn't visible at the time. // To bypass this problem we can simply recall the row selection with the same selection and that will trigger a re-apply of the CSS class // on all columns including the column we just made visible - if (this.sharedService.gridOptions.enableRowSelection && isChecked) { + if (this.gridOptions.enableRowSelection && isChecked) { const rowSelection = this.grid.getSelectedRows(); this.grid.setSelectedRows(rowSelection); } // if we're using frozen columns, we need to readjust pinning when the new hidden column becomes visible again on the left pinning container // we need to readjust frozenColumn index because SlickGrid freezes by index and has no knowledge of the columns themselves - const frozenColumnIndex = this.sharedService.gridOptions.frozenColumn ?? -1; + const frozenColumnIndex = this.gridOptions.frozenColumn ?? -1; if (frozenColumnIndex >= 0) { this.extensionUtility.readjustFrozenColumnIndexWhenNeeded(frozenColumnIndex, this.columns, visibleColumns); } + const callbackArgs = { + columnId, + showing: isChecked, + allColumns: this.columns, + visibleColumns, + columns: visibleColumns, + grid: this.grid + }; + // execute user callback when defined - if (this.controlOptions && typeof this.controlOptions.onColumnsChanged === 'function') { - this.controlOptions.onColumnsChanged(e, { - columnId, - showing: isChecked, - allColumns: this.columns, - columns: visibleColumns, - grid: this.grid - }); + this.pubSubService.publish('columnPicker:onColumnsChanged', callbackArgs); + if (typeof this.controlOptions?.onColumnsChanged === 'function') { + this.controlOptions.onColumnsChanged(e, callbackArgs); } } } @@ -363,7 +369,7 @@ export class ColumnPickerControl { } // translate all columns (including hidden columns) - this.extensionUtility.translateItems(this.sharedService.allColumns, 'nameKey', 'name'); + this.extensionUtility.translateItems(this._columns, 'nameKey', 'name'); // update the Titles of each sections (command, customTitle, ...) if (this.controlOptions) { @@ -371,7 +377,7 @@ export class ColumnPickerControl { } } - private emptyColumnPickerTitles() { + protected emptyColumnPickerTitles() { if (this.controlOptions) { this.controlOptions.columnTitle = ''; this.controlOptions.forceFitTitle = ''; diff --git a/packages/common/src/controls/gridMenu.control.ts b/packages/common/src/controls/gridMenu.control.ts new file mode 100644 index 000000000..4b3a390cd --- /dev/null +++ b/packages/common/src/controls/gridMenu.control.ts @@ -0,0 +1,1055 @@ +import { + Column, + DOMEvent, + GetSlickEventType, + GridMenu, + GridMenuCommandItemCallbackArgs, + GridMenuEventWithElementCallbackArgs, + GridMenuItem, + GridMenuOnColumnsChangedCallbackArgs, + GridMenuOption, + GridOption, + SlickEventHandler, + SlickGrid, + SlickNamespace, +} from '../interfaces/index'; +import { DelimiterType, FileType } from '../enums'; +import { ExtensionUtility } from '../extensions/extensionUtility'; +import { emptyElement, getHtmlElementOffset, getTranslationPrefix, sanitizeTextByAvailableSanitizer } from '../services'; +import { BindingEventService } from '../services/bindingEvent.service'; +import { ExcelExportService } from '../services/excelExport.service'; +import { FilterService } from '../services/filter.service'; +import { PubSubService } from '../services/pubSub.service'; +import { SharedService } from '../services/shared.service'; +import { SortService } from '../services/sort.service'; +import { TextExportService } from '../services/textExport.service'; + +// using external SlickGrid JS libraries +declare const Slick: SlickNamespace; + +/** + * A control to add a Grid Menu (hambuger menu on top-right of the grid) + * @class GridMenuControl + * @constructor + */ +export class GridMenuControl { + protected _areVisibleColumnDifferent = false; + protected _bindEventService: BindingEventService; + protected _columns: Column[] = []; + protected _columnCheckboxes: HTMLInputElement[] = []; + protected _columnTitleElm!: HTMLDivElement; + protected _customMenuElm!: HTMLDivElement; + protected _customTitleElm?: HTMLDivElement; + protected _eventHandler!: SlickEventHandler; + protected _gridMenuButtonElm!: HTMLButtonElement; + protected _gridUid = ''; + protected _headerElm?: HTMLDivElement | null; + protected _isMenuOpen = false; + protected _listElm!: HTMLSpanElement; + protected _gridMenuElm!: HTMLDivElement; + protected _userOriginalGridMenu!: GridMenu; + + protected _defaults = { + alignDropSide: 'right', + showButton: true, + hideForceFitButton: false, + hideSyncResizeButton: false, + forceFitTitle: 'Force fit columns', + marginBottom: 15, + menuWidth: 18, + contentMinWidth: 0, + resizeOnShowHeaderRow: false, + syncResizeTitle: 'Synchronous resize', + headerColumnValueExtractor: (columnDef: Column) => columnDef.name + } as GridMenuOption; + + /** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */ + constructor( + protected readonly extensionUtility: ExtensionUtility, + protected readonly filterService: FilterService, + protected readonly pubSubService: PubSubService, + protected readonly sharedService: SharedService, + protected readonly sortService: SortService, + ) { + this._bindEventService = new BindingEventService(); + this._eventHandler = new Slick.EventHandler(); + this._columns = this.sharedService.allColumns ?? []; + this._gridUid = this.grid?.getUID?.() ?? ''; + + this.initEventHandlers(); + this.init(); + } + + get eventHandler(): SlickEventHandler { + return this._eventHandler; + } + + get columns(): Column[] { + return this._columns; + } + set columns(newColumns: Column[]) { + this._columns = newColumns; + } + + get controlOptions(): GridMenu { + return this.gridOptions.gridMenu || {}; + } + set controlOptions(controlOptions: GridMenu) { + this.sharedService.gridOptions.gridMenu = controlOptions; + } + + get gridOptions(): GridOption { + return this.sharedService.gridOptions ?? {}; + } + + get grid(): SlickGrid { + return this.sharedService.slickGrid; + } + + get menuElement(): HTMLDivElement { + return this._gridMenuElm; + } + + initEventHandlers() { + // when grid columns are reordered then we also need to update/resync our picker column in the same order + const onColumnsReorderedHandler = this.grid.onColumnsReordered; + (this._eventHandler as SlickEventHandler>).subscribe(onColumnsReorderedHandler, this.updateColumnOrder.bind(this)); + + // subscribe to the grid, when it's destroyed, we should also destroy the Grid Menu + const onBeforeDestroyHandler = this.grid.onBeforeDestroy; + (this._eventHandler as SlickEventHandler>).subscribe(onBeforeDestroyHandler, this.dispose.bind(this)); + + // when a grid optionally changes from a regular grid to a frozen grid, we need to destroy & recreate the grid menu + // we do this change because the Grid Menu is on the left container for a regular grid, it should however be displayed on the right container for a frozen grid + const onSetOptionsHandler = this.grid.onSetOptions; + (this._eventHandler as SlickEventHandler>).subscribe(onSetOptionsHandler, (_e, args) => { + if (args && args.optionsBefore && args.optionsAfter) { + const switchedFromRegularToFrozen = (args.optionsBefore.frozenColumn! >= 0 && args.optionsAfter.frozenColumn === -1); + const switchedFromFrozenToRegular = (args.optionsBefore.frozenColumn === -1 && args.optionsAfter.frozenColumn! >= 0); + if (switchedFromRegularToFrozen || switchedFromFrozenToRegular) { + this.recreateGridMenu(); + } + } + }); + } + + /** Initialize plugin. */ + init() { + this._gridUid = this.grid.getUID() ?? ''; + + // keep original user grid menu, useful when switching locale to translate + this._userOriginalGridMenu = { ...this.controlOptions }; + this.controlOptions = { ...this._defaults, ...this.getDefaultGridMenuOptions(), ...this.controlOptions }; + + // merge original user grid menu items with internal items + // then sort all Grid Menu Custom Items (sorted by pointer, no need to use the return) + const originalCustomItems = this._userOriginalGridMenu && Array.isArray(this._userOriginalGridMenu.customItems) ? this._userOriginalGridMenu.customItems : []; + this.controlOptions.customItems = [...originalCustomItems, ...this.addGridMenuCustomCommands(originalCustomItems)]; + this.extensionUtility.translateItems(this.controlOptions.customItems, 'titleKey', 'title'); + this.extensionUtility.sortItems(this.controlOptions.customItems, 'positionOrder'); + + // create the Grid Menu DOM element + this.createGridMenu(); + } + + /** Dispose (destroy) the SlickGrid 3rd party plugin */ + dispose() { + this.deleteMenu(); + this._eventHandler.unsubscribeAll(); + this._bindEventService.unbindAll(); + this._listElm?.remove?.(); + this._gridMenuElm?.remove?.(); + } + + deleteMenu() { + this._bindEventService.unbindAll(); + const gridMenuElm = document.querySelector(`div.slick-gridmenu.${this._gridUid}`); + if (gridMenuElm) { + gridMenuElm.style.visibility = 'hidden'; + } + this._gridMenuButtonElm?.remove(); + this._gridMenuElm?.remove(); + this._customMenuElm?.remove(); + if (this._headerElm) { + this._headerElm.style.width = '100%'; // put back original width + } + } + + createColumnPickerContainer() { + // user could pass a title on top of the columns list + if (this.controlOptions?.columnTitle) { + this._columnTitleElm = document.createElement('div'); + this._columnTitleElm.className = 'title'; + this._columnTitleElm.textContent = this.controlOptions?.columnTitle ?? this._defaults.columnTitle; + this._gridMenuElm.appendChild(this._columnTitleElm); + } + + this._listElm = document.createElement('span'); + this._listElm.className = 'slick-gridmenu-list'; + + // update all columns on any of the column title button click from column picker + this._bindEventService.bind(this._gridMenuElm, 'click', this.handleColumnPickerItemClick.bind(this) as EventListener); + } + + createGridMenu() { + const gridMenuWidth = this.controlOptions?.menuWidth ?? this._defaults.menuWidth; + const headerSide = (this.gridOptions.hasOwnProperty('frozenColumn') && this.gridOptions.frozenColumn! >= 0) ? 'right' : 'left'; + this._headerElm = document.querySelector(`.${this._gridUid} .slick-header-${headerSide}`); + + if (this._headerElm) { + // resize the header row to include the hamburger menu icon + this._headerElm.style.width = `calc(100% - ${gridMenuWidth}px)`; + + // if header row is enabled, we also need to resize its width + const enableResizeHeaderRow = (this.controlOptions && this.controlOptions.resizeOnShowHeaderRow !== undefined) ? this.controlOptions.resizeOnShowHeaderRow : this._defaults.resizeOnShowHeaderRow; + if (enableResizeHeaderRow && this.gridOptions.showHeaderRow) { + const headerRowElm = document.querySelector(`.${this._gridUid} .slick-headerrow`); + if (headerRowElm) { + headerRowElm.style.width = `calc(100% - ${gridMenuWidth}px)`; + } + } + + const showButton = (this.controlOptions && this.controlOptions.showButton !== undefined) ? this.controlOptions.showButton : this._defaults.showButton; + if (showButton) { + this._gridMenuButtonElm = document.createElement('button'); + this._gridMenuButtonElm.className = 'slick-gridmenu-button'; + if (this.controlOptions && this.controlOptions.iconCssClass) { + this._gridMenuButtonElm.classList.add(...this.controlOptions.iconCssClass.split(' ')); + } else { + const iconImage = (this.controlOptions && this.controlOptions.iconImage) ? this.controlOptions.iconImage : ''; + const iconImageElm = document.createElement('img'); + iconImageElm.src = iconImage; + this._gridMenuButtonElm.appendChild(iconImageElm); + } + this._headerElm.parentNode?.prepend(this._gridMenuButtonElm); + + // show the Grid Menu when hamburger menu is clicked + this._bindEventService.bind(this._gridMenuButtonElm, 'click', this.showGridMenu.bind(this) as EventListener); + } + + this._gridUid = this.grid.getUID() ?? ''; + this.gridOptions.gridMenu = { ...this._defaults, ...this.controlOptions }; + + // localization support for the picker + this.translateTitleLabels(); + + this._gridMenuElm = document.createElement('div'); + this._gridMenuElm.classList.add('slick-gridmenu', this._gridUid); + this._gridMenuElm.style.visibility = 'hidden'; + + const closePickerButtonElm = document.createElement('button'); + closePickerButtonElm.className = 'close'; + closePickerButtonElm.type = 'button'; + closePickerButtonElm.dataset.dismiss = 'slick-gridmenu'; + closePickerButtonElm.setAttribute('aria-label', 'Close'); + + const closeSpanElm = document.createElement('span'); + closeSpanElm.className = 'close'; + closeSpanElm.innerHTML = '×'; + closeSpanElm.setAttribute('aria-hidden', 'true'); + + this._customMenuElm = document.createElement('div'); + this._customMenuElm.className = 'slick-gridmenu-custom'; + + closePickerButtonElm.appendChild(closeSpanElm); + this._gridMenuElm.appendChild(closePickerButtonElm); + this._gridMenuElm.appendChild(this._customMenuElm); + + this.populateCustomMenus(this.controlOptions, this._customMenuElm); + this.createColumnPickerContainer(); + + document.body.appendChild(this._gridMenuElm); + + // Hide the menu on outside click. + this._bindEventService.bind(document.body, 'mousedown', this.handleBodyMouseDown.bind(this) as EventListener); + + // destroy the picker if user leaves the page + this._bindEventService.bind(document.body, 'beforeunload', this.dispose.bind(this) as EventListener); + } + } + + /** + * Get all columns including hidden columns. + * @returns {Array} - all columns array + */ + getAllColumns() { + return this._columns; + } + + /** + * Get only the visible columns. + * @returns {Array} - only the visible columns array + */ + getVisibleColumns() { + return this.grid.getColumns(); + } + + /** + * When clicking an input checkboxes from the column picker list to show/hide a column (or from the picker extra commands like forcefit columns) + * @param event - input checkbox event + * @returns + */ + handleColumnPickerItemClick(event: DOMEvent) { + if (event.target.dataset.option === 'autoresize') { + // when calling setOptions, it will resize with ALL Columns (even the hidden ones) + // we can avoid this problem by keeping a reference to the visibleColumns before setOptions and then setColumns after + const previousVisibleColumns = this.getVisibleColumns(); + const isChecked = event.target.checked; + this.grid.setOptions({ forceFitColumns: isChecked }); + this.grid.setColumns(previousVisibleColumns); + return; + } + + if (event.target.dataset.option === 'syncresize') { + this.grid.setOptions({ syncColumnCellResize: !!(event.target.checked) }); + return; + } + + if (event.target.type === 'checkbox') { + this._areVisibleColumnDifferent = true; + const isChecked = event.target.checked; + const columnId = event.target.dataset.columnid || ''; + const visibleColumns: Column[] = []; + this._columnCheckboxes.forEach((columnCheckbox: HTMLInputElement, idx: number) => { + if (columnCheckbox.checked) { + visibleColumns.push(this.columns[idx]); + } + }); + + if (!visibleColumns.length) { + event.target.checked = true; + return; + } + + this.grid.setColumns(visibleColumns); + this.handleOnColumnsChanged(event, { + columnId, + showing: isChecked, + allColumns: this.columns, + visibleColumns, + columns: visibleColumns, + grid: this.grid + }); + } + } + + handleMenuCustomItemClick(event: Event, item: GridMenuItem) { + if (item && item.command && !item.disabled && !item.divider) { + const callbackArgs = { + grid: this.grid, + command: item.command, + item, + allColumns: this.columns, + visibleColumns: this.getVisibleColumns() + } as GridMenuCommandItemCallbackArgs; + + // execute Grid Menu callback with command, + // we'll also execute optional user defined onCommand callback when provided + this.executeGridMenuInternalCustomCommands(event, callbackArgs); + this.pubSubService.publish('gridMenu:onCommand', callbackArgs); + if (typeof this.controlOptions?.onCommand === 'function') { + this.controlOptions.onCommand(event, callbackArgs); + } + + // execute action callback when defined + if (typeof item.action === 'function') { + item.action.call(this, event, callbackArgs); + } + } + + // does the user want to leave open the Grid Menu after executing a command? + if (!this.controlOptions.leaveOpen && !event.defaultPrevented) { + this.hideMenu(event); + } + + // Stop propagation so that it doesn't register as a header click event. + event.preventDefault(); + event.stopPropagation(); + } + + /** Mouse down handler when clicking anywhere in the DOM body */ + handleBodyMouseDown(event: DOMEvent) { + if ((this._gridMenuElm !== event.target && !this._gridMenuElm.contains(event.target) && this._isMenuOpen) || event.target.className === 'close') { + this.hideMenu(event); + } + } + + /** + * Hide the Grid Menu but only if it does detect as open prior to executing anything. + * @param event + * @returns + */ + hideMenu(event: Event) { + if (this._gridMenuElm?.style?.visibility === 'visible') { + const callbackArgs = { + grid: this.grid, + menu: this._gridMenuElm, + allColumns: this.columns, + visibleColumns: this.getVisibleColumns() + } as GridMenuEventWithElementCallbackArgs; + + // execute optional callback method defined by the user, if it returns false then we won't go further neither close the menu + this.pubSubService.publish('gridMenu:onMenuClose', callbackArgs); + if (typeof this.controlOptions?.onMenuClose === 'function' && this.controlOptions.onMenuClose(event, callbackArgs) === false) { + return; + } + + this._gridMenuElm.style.visibility = 'hidden'; + this._isMenuOpen = false; + + // we also want to resize the columns if the user decided to hide certain column(s) + if (typeof this.grid?.autosizeColumns === 'function') { + // make sure that the grid still exist (by looking if the Grid UID is found in the DOM tree) + const gridUid = this.grid.getUID() || ''; + if (this._areVisibleColumnDifferent && gridUid && document.querySelector(`.${gridUid}`) !== null) { + if (this.gridOptions.enableAutoSizeColumns) { + this.grid.autosizeColumns(); + } + this._areVisibleColumnDifferent = false; + } + } + } + } + + /** + * Create and populate the Custom Menu Items and add them to the top of the DOM element (before the column picker) + * @param {GridMenu} options - grid menu options + * @param {HTMLDivElement} customMenuElm - custom menu container DOM element + */ + populateCustomMenus(options: GridMenu, customMenuElm: HTMLDivElement) { + if (Array.isArray(options?.customItems)) { + // user could pass a title on top of the custom section + if (this.controlOptions?.customTitle) { + this._customTitleElm = document.createElement('div'); + this._customTitleElm.className = 'title'; + this._customTitleElm.textContent = this.controlOptions.customTitle; + customMenuElm.appendChild(this._customTitleElm); + } + + for (const item of options.customItems) { + const callbackArgs = { + grid: this.grid, + menu: this._gridMenuElm, + columns: this.columns, + allColumns: this.getAllColumns(), + visibleColumns: this.getVisibleColumns() + } as GridMenuEventWithElementCallbackArgs; + + // run each override functions to know if the item is visible and usable + let isItemVisible = true; + let isItemUsable = true; + if (typeof item === 'object') { + isItemVisible = this.runOverrideFunctionWhenExists(item.itemVisibilityOverride, callbackArgs); + isItemUsable = this.runOverrideFunctionWhenExists(item.itemUsabilityOverride, callbackArgs); + } + + // if the result is not visible then there's no need to go further + if (!isItemVisible) { + continue; + } + + // when the override is defined, we need to use its result to update the disabled property + // so that "handleMenuItemCommandClick" has the correct flag and won't trigger a command clicked event + if (typeof item === 'object' && Object.prototype.hasOwnProperty.call(item, 'itemUsabilityOverride')) { + item.disabled = isItemUsable ? false : true; + } + + const liElm = document.createElement('li'); + liElm.className = 'slick-gridmenu-item'; + liElm.dataset.command = typeof item === 'object' && item.command || ''; + customMenuElm.appendChild(liElm); + + if ((typeof item === 'object' && item.divider) || item === 'divider') { + liElm.classList.add('slick-gridmenu-item-divider'); + continue; + } + + if (item.disabled) { + liElm.classList.add('slick-gridmenu-item-disabled'); + } + + if (item.hidden) { + liElm.classList.add('slick-gridmenu-item-hidden'); + } + + if (item.cssClass) { + liElm.classList.add(...item.cssClass.split(' ')); + } + + if (item.tooltip) { + liElm.title = item.tooltip; + } + + const iconElm = document.createElement('div'); + iconElm.className = 'slick-gridmenu-icon'; + liElm.appendChild(iconElm); + + if (item.iconCssClass) { + iconElm.classList.add(...item.iconCssClass.split(' ')); + } + + if (item.iconImage) { + console.warn('[Slickgrid-Universal] The "iconImage" property of a Grid Menu item is no deprecated and will be removed in future version, consider using "iconCssClass" instead.'); + iconElm.style.backgroundImage = `url(${item.iconImage})`; + } + + const textElm = document.createElement('span'); + textElm.className = 'slick-gridmenu-content'; + textElm.textContent = typeof item === 'object' && item.title || ''; + liElm.appendChild(textElm); + + if (item.textCssClass) { + textElm.classList.add(...item.textCssClass.split(' ')); + } + // execute command on menu item clicked + this._bindEventService.bind(liElm, 'click', (e) => this.handleMenuCustomItemClick(e, item)); + } + } + } + + recreateGridMenu() { + this.deleteMenu(); + this.init(); + } + + populateColumnPicker(e: MouseEvent, controlOptions: GridMenu) { + for (const column of this.columns) { + const columnId = column.id; + const columnLiElm = document.createElement('li'); + columnLiElm.className = column.excludeFromColumnPicker ? 'hidden' : ''; + + const colInputElm = document.createElement('input'); + colInputElm.type = 'checkbox'; + colInputElm.id = `${this._gridUid}-gridmenu-colpicker-${columnId}`; + colInputElm.dataset.columnid = `${columnId}`; + const colIndex = this.grid.getColumnIndex(columnId); + if (colIndex >= 0) { + colInputElm.checked = true; + } + columnLiElm.appendChild(colInputElm); + this._columnCheckboxes.push(colInputElm); + + const headerColumnValueExtractorFn = typeof controlOptions?.headerColumnValueExtractor === 'function' ? controlOptions.headerColumnValueExtractor : this._defaults.headerColumnValueExtractor; + const columnLabel = headerColumnValueExtractorFn!(column, this.gridOptions); + + const labelElm = document.createElement('label'); + labelElm.htmlFor = `${this._gridUid}-gridmenu-colpicker-${columnId}`; + labelElm.innerHTML = sanitizeTextByAvailableSanitizer(this.gridOptions, columnLabel); + columnLiElm.appendChild(labelElm); + this._listElm.appendChild(columnLiElm); + } + + if (!controlOptions.hideForceFitButton || !controlOptions.hideSyncResizeButton) { + this._listElm.appendChild(document.createElement('hr')); + } + + if (!(controlOptions?.hideForceFitButton)) { + const forceFitTitle = controlOptions?.forceFitTitle; + + const fitInputElm = document.createElement('input'); + fitInputElm.type = 'checkbox'; + fitInputElm.id = `${this._gridUid}-gridmenu-colpicker-forcefit`; + fitInputElm.dataset.option = 'autoresize'; + + const labelElm = document.createElement('label'); + labelElm.htmlFor = `${this._gridUid}-gridmenu-colpicker-forcefit`; + labelElm.textContent = forceFitTitle ?? ''; + if (this.gridOptions.forceFitColumns) { + fitInputElm.checked = true; + } + + const fitLiElm = document.createElement('li'); + fitLiElm.appendChild(fitInputElm); + fitLiElm.appendChild(labelElm); + this._listElm.appendChild(fitLiElm); + } + + if (!(controlOptions?.hideSyncResizeButton)) { + const syncResizeTitle = (controlOptions?.syncResizeTitle) || controlOptions.syncResizeTitle; + const labelElm = document.createElement('label'); + labelElm.htmlFor = `${this._gridUid}-gridmenu-colpicker-syncresize`; + labelElm.textContent = syncResizeTitle ?? ''; + + const syncInputElm = document.createElement('input'); + syncInputElm.type = 'checkbox'; + syncInputElm.id = `${this._gridUid}-gridmenu-colpicker-syncresize`; + syncInputElm.dataset.option = 'syncresize'; + if (this.gridOptions.syncColumnCellResize) { + syncInputElm.checked = true; + } + + const syncLiElm = document.createElement('li'); + syncLiElm.appendChild(syncInputElm); + syncLiElm.appendChild(labelElm); + this._listElm.appendChild(syncLiElm); + } + + let buttonElm = (e.target as HTMLButtonElement).nodeName === 'BUTTON' ? (e.target as HTMLButtonElement) : (e.target as HTMLElement).querySelector('button') as HTMLButtonElement; // get button element + if (!buttonElm) { + buttonElm = (e.target as HTMLElement).parentElement as HTMLButtonElement; // external grid menu might fall in this last case if wrapped in a span/div + } + const menuIconOffset = getHtmlElementOffset(buttonElm as HTMLButtonElement); + const buttonComptStyle = getComputedStyle(buttonElm as HTMLButtonElement); + const buttonWidth = parseInt(buttonComptStyle?.width ?? this._defaults?.menuWidth, 10); + + const menuWidth = this._gridMenuElm?.offsetWidth ?? 0; + const contentMinWidth = controlOptions?.contentMinWidth ?? this._defaults.contentMinWidth ?? 0; + const currentMenuWidth = ((contentMinWidth > menuWidth) ? contentMinWidth : (menuWidth)) || 0; + const nextPositionTop = menuIconOffset?.bottom ?? 0; + const nextPositionLeft = menuIconOffset?.right ?? 0; + const menuMarginBottom = ((controlOptions?.marginBottom !== undefined) ? controlOptions.marginBottom : this._defaults.marginBottom) || 0; + const calculatedLeftPosition = controlOptions?.alignDropSide === 'left' ? nextPositionLeft - buttonWidth : nextPositionLeft - currentMenuWidth; + + this._gridMenuElm.style.top = `${nextPositionTop}px`; + this._gridMenuElm.style.left = `${calculatedLeftPosition}px`; + this._gridMenuElm.classList.add(controlOptions?.alignDropSide === 'left' ? 'dropleft' : 'dropright'); + this._gridMenuElm.appendChild(this._listElm); + + if (contentMinWidth! > 0) { + this._gridMenuElm.style.minWidth = `${contentMinWidth}px`; + } + + // set 'height' when defined OR ELSE use the 'max-height' with available window size and optional margin bottom + if (controlOptions?.height !== undefined) { + this._gridMenuElm.style.height = `${controlOptions.height}px`; + } else { + this._gridMenuElm.style.maxHeight = `${window.innerHeight - e.clientY - menuMarginBottom}px`; + } + + this._gridMenuElm.style.visibility = 'visible'; + this._gridMenuElm.appendChild(this._listElm); + this._isMenuOpen = true; + } + + showGridMenu(e: MouseEvent, options?: GridMenuOption) { + e.preventDefault(); + + // empty both the picker list & the command list + emptyElement(this._listElm); + emptyElement(this._customMenuElm); + + const controlOptions: GridMenu = { ...this.controlOptions, ...options }; // merge optional picker option + this.populateCustomMenus(controlOptions, this._customMenuElm); + this.updateColumnOrder(); + this._columnCheckboxes = []; + + const callbackArgs = { + grid: this.grid, + menu: this._gridMenuElm, + allColumns: this.columns, + visibleColumns: this.getVisibleColumns() + } as GridMenuEventWithElementCallbackArgs; + + // run the override function (when defined), if the result is false then we won't go further + if (controlOptions && !this.runOverrideFunctionWhenExists(controlOptions.menuUsabilityOverride, callbackArgs)) { + return; + } + + // execute optional callback method defined by the user, if it returns false then we won't go further and not open the grid menu + if (typeof e.stopPropagation === 'function') { + this.pubSubService.publish('gridMenu:onBeforeMenuShow', callbackArgs); + if (typeof controlOptions?.onBeforeMenuShow === 'function' && controlOptions.onBeforeMenuShow(e, callbackArgs) === false) { + return; + } + } + + // load the column & create column picker list + this.populateColumnPicker(e, controlOptions); + + // execute optional callback method defined by the user + this.pubSubService.publish('gridMenu:onAfterMenuShow', callbackArgs); + if (typeof controlOptions?.onAfterMenuShow === 'function') { + controlOptions.onAfterMenuShow(e, callbackArgs); + } + } + + updateColumnOrder() { + // Because columns can be reordered, we have to update the `columns` to reflect the new order, however we can't just take `grid.getColumns()`, + // as it does not include columns currently hidden by the picker. We create a new `columns` structure by leaving currently-hidden + // columns in their original ordinal position and interleaving the results of the current column sort. + const current = this.grid.getColumns().slice(0); + const ordered = new Array(this.columns.length); + + for (let i = 0; i < ordered.length; i++) { + const columnIdx = this.grid.getColumnIndex(this.columns[i].id); + if (columnIdx === undefined) { + // if the column doesn't return a value from getColumnIndex, it is hidden. Leave it in this position. + ordered[i] = this.columns[i]; + } else { + // otherwise, grab the next visible column. + ordered[i] = current.shift(); + } + } + + // the new set of ordered columns becomes the new set of column picker columns + this._columns = ordered; + } + + /** Update the Titles of each sections (command, customTitle, ...) */ + updateAllTitles(options: GridMenuOption) { + if (this._columnTitleElm?.textContent && options.customTitle) { + this._columnTitleElm.textContent = options.customTitle; + } + if (this._columnTitleElm?.textContent && options.columnTitle) { + this._columnTitleElm.textContent = options.columnTitle; + } + } + + /** Translate the Grid Menu titles and column picker */ + translateGridMenu() { + // update the properties by pointers, that is the only way to get Grid Menu Control to see the new values + // we also need to call the control init so that it takes the new Grid object with latest values + if (this.controlOptions) { + this.controlOptions.customItems = []; + this.emptyGridMenuTitles(); + + // merge original user grid menu items with internal items + // then sort all Grid Menu Custom Items (sorted by pointer, no need to use the return) + const originalCustomItems = this._userOriginalGridMenu && Array.isArray(this._userOriginalGridMenu.customItems) ? this._userOriginalGridMenu.customItems : []; + this.controlOptions.customItems = [...originalCustomItems, ...this.addGridMenuCustomCommands(originalCustomItems)]; + this.extensionUtility.translateItems(this.controlOptions.customItems, 'titleKey', 'title'); + this.extensionUtility.sortItems(this.controlOptions.customItems, 'positionOrder'); + this.translateTitleLabels(); + + // translate all columns (including non-visible) + this.extensionUtility.translateItems(this._columns, 'nameKey', 'name'); + + // update the Titles of each sections (command, customTitle, ...) + this.updateAllTitles(this.controlOptions); + } + } + + translateTitleLabels() { + this.controlOptions.columnTitle = this.extensionUtility.getPickerTitleOutputString('columnTitle', 'gridMenu'); + this.controlOptions.forceFitTitle = this.extensionUtility.getPickerTitleOutputString('forceFitTitle', 'gridMenu'); + this.controlOptions.syncResizeTitle = this.extensionUtility.getPickerTitleOutputString('syncResizeTitle', 'gridMenu'); + } + + // -- + // protected functions + // ------------------ + + protected emptyGridMenuTitles() { + if (this.controlOptions) { + this.controlOptions.customTitle = ''; + this.controlOptions.columnTitle = ''; + this.controlOptions.forceFitTitle = ''; + this.controlOptions.syncResizeTitle = ''; + } + } + + /** Create Grid Menu with Custom Commands if user has enabled Filters and/or uses a Backend Service (OData, GraphQL) */ + protected addGridMenuCustomCommands(originalCustomItems: Array) { + const backendApi = this.gridOptions.backendServiceApi || null; + const gridMenuCustomItems: Array = []; + const gridOptions = this.gridOptions; + const translationPrefix = getTranslationPrefix(gridOptions); + const commandLabels = this.controlOptions?.commandLabels; + + // show grid menu: Unfreeze Columns/Rows + if (this.gridOptions && this.controlOptions && !this.controlOptions.hideClearFrozenColumnsCommand) { + const commandName = 'clear-pinning'; + if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.controlOptions.iconClearFrozenColumnsCommand || 'fa fa-times', + title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.clearFrozenColumnsCommandKey}`, 'TEXT_CLEAR_PINNING', commandLabels?.clearFrozenColumnsCommand), + disabled: false, + command: commandName, + positionOrder: 52 + } + ); + } + } + + if (this.gridOptions && (this.gridOptions.enableFiltering && !this.sharedService.hideHeaderRowAfterPageLoad)) { + // show grid menu: Clear all Filters + if (this.gridOptions && this.controlOptions && !this.controlOptions.hideClearAllFiltersCommand) { + const commandName = 'clear-filter'; + if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.controlOptions.iconClearAllFiltersCommand || 'fa fa-filter text-danger', + title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.clearAllFiltersCommandKey}`, 'TEXT_CLEAR_ALL_FILTERS', commandLabels?.clearAllFiltersCommand), + disabled: false, + command: commandName, + positionOrder: 50 + } + ); + } + } + + // show grid menu: toggle filter row + if (this.gridOptions && this.controlOptions && !this.controlOptions.hideToggleFilterCommand) { + const commandName = 'toggle-filter'; + if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.controlOptions.iconToggleFilterCommand || 'fa fa-random', + title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.toggleFilterCommandKey}`, 'TEXT_TOGGLE_FILTER_ROW', commandLabels?.toggleFilterCommand), + disabled: false, + command: commandName, + positionOrder: 53 + } + ); + } + } + + // show grid menu: refresh dataset + if (backendApi && this.gridOptions && this.controlOptions && !this.controlOptions.hideRefreshDatasetCommand) { + const commandName = 'refresh-dataset'; + if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.controlOptions.iconRefreshDatasetCommand || 'fa fa-refresh', + title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.refreshDatasetCommandKey}`, 'TEXT_REFRESH_DATASET', commandLabels?.refreshDatasetCommand), + disabled: false, + command: commandName, + positionOrder: 57 + } + ); + } + } + } + + if (this.gridOptions.showPreHeaderPanel) { + // show grid menu: toggle pre-header row + if (this.gridOptions && this.controlOptions && !this.controlOptions.hideTogglePreHeaderCommand) { + const commandName = 'toggle-preheader'; + if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.controlOptions.iconTogglePreHeaderCommand || 'fa fa-random', + title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.togglePreHeaderCommandKey}`, 'TEXT_TOGGLE_PRE_HEADER_ROW', commandLabels?.togglePreHeaderCommand), + disabled: false, + command: commandName, + positionOrder: 53 + } + ); + } + } + } + + if (this.gridOptions.enableSorting) { + // show grid menu: Clear all Sorting + if (this.gridOptions && this.controlOptions && !this.controlOptions.hideClearAllSortingCommand) { + const commandName = 'clear-sorting'; + if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.controlOptions.iconClearAllSortingCommand || 'fa fa-unsorted text-danger', + title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.clearAllSortingCommandKey}`, 'TEXT_CLEAR_ALL_SORTING', commandLabels?.clearAllSortingCommand), + disabled: false, + command: commandName, + positionOrder: 51 + } + ); + } + } + } + + // show grid menu: Export to file + if ((this.gridOptions?.enableExport || this.gridOptions?.enableTextExport) && this.controlOptions && !this.controlOptions.hideExportCsvCommand) { + const commandName = 'export-csv'; + if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.controlOptions.iconExportCsvCommand || 'fa fa-download', + title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.exportCsvCommandKey}`, 'TEXT_EXPORT_TO_CSV', commandLabels?.exportCsvCommand), + disabled: false, + command: commandName, + positionOrder: 54 + } + ); + } + } + + // show grid menu: Export to Excel + if (this.gridOptions && this.gridOptions.enableExcelExport && this.controlOptions && !this.controlOptions.hideExportExcelCommand) { + const commandName = 'export-excel'; + if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.controlOptions.iconExportExcelCommand || 'fa fa-file-excel-o text-success', + title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.exportExcelCommandKey}`, 'TEXT_EXPORT_TO_EXCEL', commandLabels?.exportExcelCommand), + disabled: false, + command: commandName, + positionOrder: 55 + } + ); + } + } + + // show grid menu: export to text file as tab delimited + if ((this.gridOptions?.enableExport || this.gridOptions?.enableTextExport) && this.controlOptions && !this.controlOptions.hideExportTextDelimitedCommand) { + const commandName = 'export-text-delimited'; + if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.controlOptions.iconExportTextDelimitedCommand || 'fa fa-download', + title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.exportTextDelimitedCommandKey}`, 'TEXT_EXPORT_TO_TAB_DELIMITED', commandLabels?.exportTextDelimitedCommand), + disabled: false, + command: commandName, + positionOrder: 56 + } + ); + } + } + + // add the custom "Commands" title if there are any commands + if (this.gridOptions && this.controlOptions && (Array.isArray(gridMenuCustomItems) && gridMenuCustomItems.length > 0 || (Array.isArray(this.controlOptions.customItems) && this.controlOptions.customItems.length > 0))) { + this.controlOptions.customTitle = this.controlOptions.customTitle || this.extensionUtility.getPickerTitleOutputString('customTitle', 'gridMenu'); + } + + return gridMenuCustomItems; + } + + /** + * Execute the Grid Menu Custom command callback that was triggered by the onCommand subscribe + * These are the default internal custom commands + * @param event + * @param GridMenuItem args + */ + protected executeGridMenuInternalCustomCommands(_e: Event, args: GridMenuItem) { + const registeredResources = this.sharedService?.externalRegisteredResources || []; + + if (args?.command) { + switch (args.command) { + case 'clear-pinning': + const visibleColumns = [...this.sharedService.visibleColumns]; + const newGridOptions = { frozenColumn: -1, frozenRow: -1, frozenBottom: false, enableMouseWheelScrollHandler: false }; + this.grid.setOptions(newGridOptions); + this.gridOptions.frozenColumn = newGridOptions.frozenColumn; + this.gridOptions.frozenRow = newGridOptions.frozenRow; + this.gridOptions.frozenBottom = newGridOptions.frozenBottom; + this.gridOptions.enableMouseWheelScrollHandler = newGridOptions.enableMouseWheelScrollHandler; + + // SlickGrid seems to be somehow resetting the columns to their original positions, + // so let's re-fix them to the position we kept as reference + if (Array.isArray(visibleColumns)) { + this.grid.setColumns(visibleColumns); + } + + // we also need to autosize columns if the option is enabled + const gridOptions = this.gridOptions; + if (gridOptions.enableAutoSizeColumns) { + this.grid.autosizeColumns(); + } + break; + case 'clear-filter': + this.filterService.clearFilters(); + this.sharedService.dataView.refresh(); + break; + case 'clear-sorting': + this.sortService.clearSorting(); + this.sharedService.dataView.refresh(); + break; + case 'export-csv': + const exportCsvService: TextExportService = registeredResources.find((service: any) => service.className === 'TextExportService'); + if (exportCsvService?.exportToFile) { + exportCsvService.exportToFile({ + delimiter: DelimiterType.comma, + format: FileType.csv, + }); + } else { + console.error(`[Slickgrid-Universal] You must register the TextExportService to properly use Export to File in the Grid Menu. Example:: this.gridOptions = { enableTextExport: true, registerExternalResources: [new TextExportService()] };`); + } + break; + case 'export-excel': + const excelService: ExcelExportService = registeredResources.find((service: any) => service.className === 'ExcelExportService'); + if (excelService?.exportToExcel) { + excelService.exportToExcel(); + } else { + console.error(`[Slickgrid-Universal] You must register the ExcelExportService to properly use Export to Excel in the Grid Menu. Example:: this.gridOptions = { enableExcelExport: true, registerExternalResources: [new ExcelExportService()] };`); + } + break; + case 'export-text-delimited': + const exportTxtService: TextExportService = registeredResources.find((service: any) => service.className === 'TextExportService'); + if (exportTxtService?.exportToFile) { + exportTxtService.exportToFile({ + delimiter: DelimiterType.tab, + format: FileType.txt, + }); + } else { + console.error(`[Slickgrid-Universal] You must register the TextExportService to properly use Export to File in the Grid Menu. Example:: this.gridOptions = { enableTextExport: true, registerExternalResources: [new TextExportService()] };`); + } + break; + case 'toggle-filter': + let showHeaderRow = this.gridOptions?.showHeaderRow ?? false; + showHeaderRow = !showHeaderRow; // inverse show header flag + this.grid.setHeaderRowVisibility(showHeaderRow); + + // when displaying header row, we'll call "setColumns" which in terms will recreate the header row filters + if (showHeaderRow === true) { + this.grid.setColumns(this.sharedService.columnDefinitions); + this.grid.scrollColumnIntoView(0); // quick fix to avoid filter being out of sync with horizontal scroll + } + break; + case 'toggle-preheader': + const showPreHeaderPanel = this.gridOptions?.showPreHeaderPanel ?? false; + this.grid.setPreHeaderPanelVisibility(!showPreHeaderPanel); + break; + case 'refresh-dataset': + this.extensionUtility.refreshBackendDataset(); + break; + default: + break; + } + } + } + + /** @return default Grid Menu options */ + protected getDefaultGridMenuOptions(): GridMenu { + return { + customTitle: undefined, + columnTitle: this.extensionUtility.getPickerTitleOutputString('columnTitle', 'gridMenu'), + forceFitTitle: this.extensionUtility.getPickerTitleOutputString('forceFitTitle', 'gridMenu'), + syncResizeTitle: this.extensionUtility.getPickerTitleOutputString('syncResizeTitle', 'gridMenu'), + iconCssClass: 'fa fa-bars', + menuWidth: 18, + customItems: [], + hideClearAllFiltersCommand: false, + hideRefreshDatasetCommand: false, + hideToggleFilterCommand: false, + }; + } + + protected handleOnColumnsChanged(e: DOMEvent, args: GridMenuOnColumnsChangedCallbackArgs) { + // execute optional callback method defined by the user + this.pubSubService.publish('gridMenu:onColumnsChanged', args); + if (typeof this.controlOptions?.onColumnsChanged === 'function') { + this.controlOptions.onColumnsChanged(e, args); + } + + // keep reference to the updated visible columns list + if (args && Array.isArray(args.visibleColumns) && args.visibleColumns.length > this.sharedService.visibleColumns.length) { + this.sharedService.visibleColumns = args.visibleColumns; + } + + // when using row selection, SlickGrid will only apply the "selected" CSS class on the visible columns only + // and if the row selection was done prior to the column being shown then that column that was previously hidden (at the time of the row selection) + // will not have the "selected" CSS class because it wasn't visible at the time. + // To bypass this problem we can simply recall the row selection with the same selection and that will trigger a re-apply of the CSS class + // on all columns including the column we just made visible + if (this.gridOptions.enableRowSelection && args.showing) { + const rowSelection = args.grid.getSelectedRows(); + args.grid.setSelectedRows(rowSelection); + } + + // if we're using frozen columns, we need to readjust pinning when the new hidden column becomes visible again on the left pinning container + // we need to readjust frozenColumn index because SlickGrid freezes by index and has no knowledge of the columns themselves + const frozenColumnIndex = this.gridOptions.frozenColumn ?? -1; + if (frozenColumnIndex >= 0) { + const { allColumns, visibleColumns } = args; + this.extensionUtility.readjustFrozenColumnIndexWhenNeeded(frozenColumnIndex, allColumns, visibleColumns); + } + } + + /** Run the Override function when it exists, if it returns True then it is usable/visible */ + protected runOverrideFunctionWhenExists(overrideFn: any, args: any): boolean { + if (typeof overrideFn === 'function') { + return overrideFn.call(this, args); + } + return true; + } +} \ No newline at end of file diff --git a/packages/common/src/controls/index.ts b/packages/common/src/controls/index.ts index 9cc5bd1a6..b8b783f40 100644 --- a/packages/common/src/controls/index.ts +++ b/packages/common/src/controls/index.ts @@ -1 +1,2 @@ -export * from './columnPicker.control'; \ No newline at end of file +export * from './columnPicker.control'; +export * from './gridMenu.control'; \ No newline at end of file diff --git a/packages/common/src/extensions/__tests__/cellExternalCopyManagerExtension.spec.ts b/packages/common/src/extensions/__tests__/cellExternalCopyManagerExtension.spec.ts index 8f8fc98b6..6cdf94252 100644 --- a/packages/common/src/extensions/__tests__/cellExternalCopyManagerExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/cellExternalCopyManagerExtension.spec.ts @@ -4,6 +4,7 @@ import { CellExternalCopyManagerExtension } from '../cellExternalCopyManagerExte import { ExtensionUtility } from '../extensionUtility'; import { SharedService } from '../../services/shared.service'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; +import { BackendUtilityService } from '../../services'; declare const Slick: SlickNamespace; jest.mock('flatpickr', () => { }); @@ -41,6 +42,7 @@ describe('cellExternalCopyManagerExtension', () => { let extension: CellExternalCopyManagerExtension; let extensionUtility: ExtensionUtility; + let backendUtilityService: BackendUtilityService; let sharedService: SharedService; let translateService: TranslateServiceStub; const gridOptionsMock = { @@ -56,8 +58,9 @@ describe('cellExternalCopyManagerExtension', () => { beforeEach(() => { sharedService = new SharedService(); + backendUtilityService = new BackendUtilityService(); translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); extension = new CellExternalCopyManagerExtension(extensionUtility, sharedService); }); diff --git a/packages/common/src/extensions/__tests__/cellMenuExtension.spec.ts b/packages/common/src/extensions/__tests__/cellMenuExtension.spec.ts index 32a852cc0..d1e2cf205 100644 --- a/packages/common/src/extensions/__tests__/cellMenuExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/cellMenuExtension.spec.ts @@ -3,6 +3,7 @@ import { ExtensionUtility } from '../extensionUtility'; import { SharedService } from '../../services/shared.service'; import { Column, SlickDataView, GridOption, SlickGrid, SlickNamespace, CellMenu, SlickCellMenu } from '../../interfaces/index'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; +import { BackendUtilityService } from '../../services'; declare const Slick: SlickNamespace; @@ -43,6 +44,7 @@ describe('CellMenuExtension', () => { const columnsMock: Column[] = [{ id: 'field1', field: 'field1', width: 100, nameKey: 'TITLE' }, { id: 'field2', field: 'field2', width: 75 }]; let extensionUtility: ExtensionUtility; + let backendUtilityService: BackendUtilityService; let translateService: TranslateServiceStub; let extension: CellMenuExtension; let sharedService: SharedService; @@ -87,8 +89,9 @@ describe('CellMenuExtension', () => { describe('with I18N Service', () => { beforeEach(() => { sharedService = new SharedService(); + backendUtilityService = new BackendUtilityService(); translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); extension = new CellMenuExtension(extensionUtility, sharedService, translateService); translateService.use('fr'); }); diff --git a/packages/common/src/extensions/__tests__/checkboxSelectorExtension.spec.ts b/packages/common/src/extensions/__tests__/checkboxSelectorExtension.spec.ts index e6d78c1dd..62e0bedd4 100644 --- a/packages/common/src/extensions/__tests__/checkboxSelectorExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/checkboxSelectorExtension.spec.ts @@ -3,6 +3,7 @@ import { CheckboxSelectorExtension } from '../checkboxSelectorExtension'; import { ExtensionUtility } from '../extensionUtility'; import { SharedService } from '../../services/shared.service'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; +import { BackendUtilityService } from '../../services'; jest.useFakeTimers(); @@ -43,6 +44,7 @@ describe('checkboxSelectorExtension', () => { Slick.RowSelectionModel = mockSelectionModel; let extension: CheckboxSelectorExtension; + let backendUtilityService: BackendUtilityService; let extensionUtility: ExtensionUtility; let sharedService: SharedService; let translateService: TranslateServiceStub; @@ -50,8 +52,9 @@ describe('checkboxSelectorExtension', () => { beforeEach(() => { sharedService = new SharedService(); + backendUtilityService = new BackendUtilityService(); translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); extension = new CheckboxSelectorExtension(sharedService); }); diff --git a/packages/common/src/extensions/__tests__/contextMenuExtension.spec.ts b/packages/common/src/extensions/__tests__/contextMenuExtension.spec.ts index 06205c348..c37bbd248 100644 --- a/packages/common/src/extensions/__tests__/contextMenuExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/contextMenuExtension.spec.ts @@ -7,7 +7,7 @@ import { SharedService } from '../../services/shared.service'; import { DelimiterType, FileType } from '../../enums/index'; import { Column, SlickDataView, GridOption, MenuCommandItem, SlickGrid, SlickNamespace, ContextMenu, SlickContextMenu } from '../../interfaces/index'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; -import { ExcelExportService, TextExportService, TreeDataService } from '../../services'; +import { BackendUtilityService, ExcelExportService, TextExportService, TreeDataService } from '../../services'; declare const Slick: SlickNamespace; @@ -74,6 +74,7 @@ describe('contextMenuExtension', () => { const columnsMock: Column[] = [{ id: 'field1', field: 'field1', width: 100, nameKey: 'TITLE' }, { id: 'field2', field: 'field2', width: 75 }]; let extensionUtility: ExtensionUtility; + let backendUtilityService: BackendUtilityService; let translateService: TranslateServiceStub; let extension: ContextMenuExtension; let sharedService: SharedService; @@ -124,8 +125,9 @@ describe('contextMenuExtension', () => { describe('with I18N Service', () => { beforeEach(() => { sharedService = new SharedService(); + backendUtilityService = new BackendUtilityService(); translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); extension = new ContextMenuExtension(extensionUtility, sharedService, treeDataServiceStub, translateService); translateService.use('fr'); }); diff --git a/packages/common/src/extensions/__tests__/draggableGroupingExtension.spec.ts b/packages/common/src/extensions/__tests__/draggableGroupingExtension.spec.ts index 7030995ef..74c33ed3c 100644 --- a/packages/common/src/extensions/__tests__/draggableGroupingExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/draggableGroupingExtension.spec.ts @@ -3,6 +3,7 @@ import { DraggableGroupingExtension } from '../draggableGroupingExtension'; import { ExtensionUtility } from '../extensionUtility'; import { SharedService } from '../../services/shared.service'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; +import { BackendUtilityService } from '../../services'; declare const Slick: SlickNamespace; @@ -22,6 +23,7 @@ describe('draggableGroupingExtension', () => { Slick.DraggableGrouping = mockAddon; let extensionUtility: ExtensionUtility; + let backendUtilityService: BackendUtilityService; let sharedService: SharedService; let extension: DraggableGroupingExtension; let translateService: TranslateServiceStub; @@ -38,8 +40,9 @@ describe('draggableGroupingExtension', () => { beforeEach(() => { sharedService = new SharedService(); + backendUtilityService = new BackendUtilityService(); translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); extension = new DraggableGroupingExtension(extensionUtility, sharedService); }); diff --git a/packages/common/src/extensions/__tests__/extensionUtility.spec.ts b/packages/common/src/extensions/__tests__/extensionUtility.spec.ts index d4dc34853..f7de692c9 100644 --- a/packages/common/src/extensions/__tests__/extensionUtility.spec.ts +++ b/packages/common/src/extensions/__tests__/extensionUtility.spec.ts @@ -1,6 +1,7 @@ import { Column, GridOption, SlickGrid } from '../../interfaces/index'; import { ExtensionUtility } from '../extensionUtility'; import { SharedService } from '../../services/shared.service'; +import { BackendUtilityService } from '../../services/backendUtility.service'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; const gridStub = { @@ -27,25 +28,12 @@ jest.mock('slickgrid/plugins/slick.rowselectionmodel', () => mockAddon); jest.mock('slickgrid/plugins/slick.rowdetailview', () => mockAddon); jest.mock('slickgrid/plugins/slick.rowmovemanager', () => mockAddon); -const Slick = { - DraggableGrouping: mockAddon, - RowMoveManager: mockAddon, - RowSelectionModel: mockAddon, - Controls: { - GridMenu: mockAddon, - }, - Data: { - GroupItemMetadataProvider: mockAddon - }, - Plugins: { - CellMenu: mockAddon, - ContextMenu: mockAddon, - CellExternalCopyManager: mockAddon, - HeaderButtons: mockAddon, - HeaderMenu: mockAddon, - RowDetailView: mockAddon, - } -}; +const backendUtilityServiceStub = { + executeBackendProcessesCallback: jest.fn(), + executeBackendCallback: jest.fn(), + onBackendError: jest.fn(), + refreshBackendDataset: jest.fn(), +} as unknown as BackendUtilityService; describe('extensionUtility', () => { let sharedService: SharedService; @@ -56,7 +44,7 @@ describe('extensionUtility', () => { beforeEach(async () => { sharedService = new SharedService(); translateService = new TranslateServiceStub(); - utility = new ExtensionUtility(sharedService, translateService); + utility = new ExtensionUtility(sharedService, backendUtilityServiceStub, translateService); await translateService.use('fr'); }); @@ -80,6 +68,27 @@ describe('extensionUtility', () => { }); }); + describe('refreshBackendDataset method', () => { + let gridOptionsMock; + + beforeEach(() => { + gridOptionsMock = { enableTranslate: true, enableGridMenu: true } as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + }); + + it('should call refresh of backend when method is called', () => { + const refreshSpy = jest.spyOn(backendUtilityServiceStub, 'refreshBackendDataset'); + utility.refreshBackendDataset(); + expect(refreshSpy).toHaveBeenCalledWith(gridOptionsMock); + }); + + it('should call refresh of backend when method is called', () => { + const refreshSpy = jest.spyOn(backendUtilityServiceStub, 'refreshBackendDataset'); + utility.refreshBackendDataset({ enablePagination: true }); + expect(refreshSpy).toHaveBeenCalledWith({ ...gridOptionsMock, enablePagination: true }); + }); + }); + describe('sortItems method', () => { it('should sort the items by their order property', () => { const inputArray = [{ field: 'field1', order: 3 }, { field: 'field2', order: 1 }, { field: 'field3', order: 2 }]; @@ -176,7 +185,7 @@ describe('extensionUtility', () => { describe('without Translate Service', () => { beforeEach(() => { translateService = undefined as any; - utility = new ExtensionUtility(sharedService, translateService); + utility = new ExtensionUtility(sharedService, backendUtilityServiceStub, translateService); }); it('should throw an error if "enableTranslate" is set but the I18N Service is null', () => { diff --git a/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts b/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts deleted file mode 100644 index d4a39767b..000000000 --- a/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts +++ /dev/null @@ -1,864 +0,0 @@ -import { DelimiterType, FileType } from '../../enums/index'; -import { Column, SlickDataView, GridOption, SlickGrid, SlickNamespace, GridMenu, SlickGridMenu, BackendServiceApi } from '../../interfaces/index'; -import { GridMenuExtension } from '../gridMenuExtension'; -import { ExtensionUtility } from '../extensionUtility'; -import { SharedService } from '../../services/shared.service'; -import { ExcelExportService, TextExportService, FilterService, SortService, BackendUtilityService } from '../../services'; -import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; - -declare const Slick: SlickNamespace; -jest.mock('flatpickr', () => { }); - -const gridId = 'grid1'; -const gridUid = 'slickgrid_124343'; -const containerId = 'demo-container'; - -const excelExportServiceStub = { - className: 'ExcelExportService', - exportToExcel: jest.fn(), -} as unknown as ExcelExportService; - -const exportServiceStub = { - className: 'TextExportService', - exportToFile: jest.fn(), -} as unknown as TextExportService; - -const filterServiceStub = { - clearFilters: jest.fn(), -} as unknown as FilterService; - -const sortServiceStub = { - clearSorting: jest.fn(), -} as unknown as SortService; - -const dataViewStub = { - refresh: jest.fn(), -} as unknown as SlickDataView; - -const gridStub = { - autosizeColumns: jest.fn(), - getColumnIndex: jest.fn(), - getColumns: jest.fn(), - getOptions: jest.fn(), - getSelectedRows: jest.fn(), - getUID: () => gridUid, - registerPlugin: jest.fn(), - setColumns: jest.fn(), - setHeaderRowVisibility: jest.fn(), - setSelectedRows: jest.fn(), - setTopPanelVisibility: jest.fn(), - setPreHeaderPanelVisibility: jest.fn(), - setOptions: jest.fn(), - scrollColumnIntoView: jest.fn(), -} as unknown as SlickGrid; - -const mockGridMenuAddon = { - init: jest.fn(), - destroy: jest.fn(), - showGridMenu: jest.fn(), - updateAllTitles: jest.fn(), - onColumnsChanged: new Slick.Event(), - onCommand: new Slick.Event(), - onAfterMenuShow: new Slick.Event(), - onBeforeMenuShow: new Slick.Event(), - onMenuClose: new Slick.Event(), -}; -const mockAddon = jest.fn().mockImplementation(() => mockGridMenuAddon); - -// define a
container to simulate the grid container -const template = - `
-
-
-
-
`; - -describe('gridMenuExtension', () => { - jest.mock('slickgrid/controls/slick.gridmenu', () => mockAddon); - Slick.Controls = { GridMenu: mockAddon } as any; - - const columnsMock: Column[] = [{ id: 'field1', field: 'field1', width: 100, nameKey: 'TITLE' }, { id: 'field2', field: 'field2', width: 75 }]; - let divElement: HTMLDivElement; - let backendUtilityService: BackendUtilityService; - let extensionUtility: ExtensionUtility; - let translateService: TranslateServiceStub; - let extension: GridMenuExtension; - let sharedService: SharedService; - - const gridOptionsMock = { - enableAutoSizeColumns: true, - enableGridMenu: true, - enableTranslate: true, - backendServiceApi: { - service: { - buildQuery: jest.fn(), - }, - internalPostProcess: jest.fn(), - preProcess: jest.fn(), - process: jest.fn(), - postProcess: jest.fn(), - }, - gridMenu: { - commandLabels: { - clearAllFiltersCommandKey: 'CLEAR_ALL_FILTERS', - clearAllSortingCommandKey: 'CLEAR_ALL_SORTING', - clearFrozenColumnsCommandKey: 'CLEAR_PINNING', - exportCsvCommandKey: 'EXPORT_TO_CSV', - exportExcelCommandKey: 'EXPORT_TO_EXCEL', - exportTextDelimitedCommandKey: 'EXPORT_TO_TAB_DELIMITED', - refreshDatasetCommandKey: 'REFRESH_DATASET', - toggleFilterCommandKey: 'TOGGLE_FILTER_ROW', - togglePreHeaderCommandKey: 'TOGGLE_PRE_HEADER_ROW', - }, - customItems: [], - hideClearAllFiltersCommand: false, - hideClearFrozenColumnsCommand: true, - hideForceFitButton: false, - hideSyncResizeButton: true, - onExtensionRegistered: jest.fn(), - onCommand: () => { }, - onColumnsChanged: () => { }, - onAfterMenuShow: () => { }, - onBeforeMenuShow: () => { }, - onMenuClose: () => { }, - }, - pagination: { - totalItems: 0 - }, - showHeaderRow: false, - showTopPanel: false, - showPreHeaderPanel: false - } as unknown as GridOption; - - describe('with I18N Service', () => { - beforeEach(() => { - const div = document.createElement('div'); - divElement = document.createElement('div'); - div.innerHTML = template; - document.body.appendChild(div); - - backendUtilityService = new BackendUtilityService(); - sharedService = new SharedService(); - translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); - extension = new GridMenuExtension(extensionUtility, filterServiceStub, sharedService, sortServiceStub, backendUtilityService, translateService); - translateService.use('fr'); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should return null when either the grid object or the grid options is missing', () => { - const output = extension.register(); - expect(output).toBeNull(); - }); - - describe('registered addon', () => { - beforeEach(() => { - jest.spyOn(SharedService.prototype, 'dataView', 'get').mockReturnValue(dataViewStub); - jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(columnsMock); - jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(columnsMock.slice(0, 1)); - jest.spyOn(SharedService.prototype, 'columnDefinitions', 'get').mockReturnValue(columnsMock); - }); - - it('should register the addon', () => { - const onRegisteredSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onExtensionRegistered'); - const instance = extension.register() as SlickGridMenu; - const addonInstance = extension.getAddonInstance(); - - expect(instance).toBeTruthy(); - expect(instance).toEqual(addonInstance); - expect(onRegisteredSpy).toHaveBeenCalledWith(instance); - expect(mockAddon).toHaveBeenCalledWith(columnsMock, gridStub, gridOptionsMock); - }); - - it('should call internal event handler subscribe and expect the "onColumnsChanged" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onColumnsChanged'); - const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onBeforeMenuShow'); - const onAfterSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onAfterMenuShow'); - const onCloseSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onMenuClose'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - const visibleColsSpy = jest.spyOn(SharedService.prototype, 'visibleColumns', 'set'); - const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); - - const instance = extension.register() as SlickGridMenu; - instance.onColumnsChanged!.notify({ columnId: 'field1', showing: false, allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }, new Slick.EventData(), gridStub); - - expect(readjustSpy).not.toHaveBeenCalled(); - expect(handlerSpy).toHaveBeenCalledTimes(5); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { columnId: 'field1', showing: false, allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }); - expect(onAfterSpy).not.toHaveBeenCalled(); - expect(onBeforeSpy).not.toHaveBeenCalled(); - expect(onCloseSpy).not.toHaveBeenCalled(); - expect(onCommandSpy).not.toHaveBeenCalled(); - expect(visibleColsSpy).not.toHaveBeenCalled(); - }); - - it(`should call internal event handler subscribe and expect the "onColumnsChanged" option to be called - and it should override "visibleColumns" when array passed as arguments is bigger than previous visible columns`, () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onColumnsChanged'); - const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onBeforeMenuShow'); - const onAfterSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onAfterMenuShow'); - const onCloseSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onMenuClose'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - const visibleColsSpy = jest.spyOn(SharedService.prototype, 'visibleColumns', 'set'); - - const instance = extension.register() as SlickGridMenu; - instance.onColumnsChanged!.notify({ columnId: 'field1', showing: true, allColumns: columnsMock, columns: columnsMock, grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(5); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { columnId: 'field1', showing: true, allColumns: columnsMock, columns: columnsMock, grid: gridStub }); - expect(onAfterSpy).not.toHaveBeenCalled(); - expect(onBeforeSpy).not.toHaveBeenCalled(); - expect(onCloseSpy).not.toHaveBeenCalled(); - expect(onCommandSpy).not.toHaveBeenCalled(); - expect(visibleColsSpy).toHaveBeenCalledWith(columnsMock); - }); - - it('should call internal "onColumnsChanged" event and expect "setSelectedRows" method to be called using Row Selection is enabled', () => { - const mockRowSelection = [0, 3, 5]; - - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue(mockRowSelection); - const setSelectionSpy = jest.spyOn(gridStub, 'setSelectedRows'); - - gridOptionsMock.enableRowSelection = true; - const instance = extension.register(); - instance.onColumnsChanged.notify({ columnId: 'field1', showing: true, allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(5); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(setSelectionSpy).toHaveBeenCalledWith(mockRowSelection); - }); - - it('should call internal "onColumnsChanged" event and expect "readjustFrozenColumnIndexWhenNeeded" method to be called when the grid is detected to be a frozen grid', () => { - gridOptionsMock.frozenColumn = 0; - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); - - const instance = extension.register() as SlickGridMenu; - instance.onColumnsChanged.notify({ columnId: 'field1', showing: false, allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(5); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(readjustSpy).toHaveBeenCalledWith(0, columnsMock, columnsMock.slice(0, 1)); - }); - - it('should call internal event handler subscribe and expect the "onBeforeMenuShow" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onColumnsChanged'); - const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onBeforeMenuShow'); - const onAfterSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onAfterMenuShow'); - const onCloseSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onMenuClose'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - - const instance = extension.register() as SlickGridMenu; - instance.onBeforeMenuShow!.notify({ columns: [], grid: gridStub, menu: divElement }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(5); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onBeforeSpy).toHaveBeenCalledWith(expect.anything(), { columns: [], grid: gridStub, menu: divElement }); - expect(onAfterSpy).not.toHaveBeenCalled(); - expect(onColumnSpy).not.toHaveBeenCalled(); - expect(onCloseSpy).not.toHaveBeenCalled(); - expect(onCommandSpy).not.toHaveBeenCalled(); - }); - - it('should call internal event handler subscribe and expect the "onMenuClose" option to be called when addon notify is called', () => { - jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onColumnsChanged'); - const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onBeforeMenuShow'); - const onAfterSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onAfterMenuShow'); - const onCloseSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onMenuClose'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - - const instance = extension.register() as SlickGridMenu; - instance.onMenuClose!.notify({ allColumns: [], visibleColumns: [], grid: gridStub, menu: divElement }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(5); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onCloseSpy).toHaveBeenCalledWith(expect.anything(), { allColumns: [], visibleColumns: [], grid: gridStub, menu: divElement }); - expect(onAfterSpy).not.toHaveBeenCalled(); - expect(onColumnSpy).not.toHaveBeenCalled(); - expect(onBeforeSpy).not.toHaveBeenCalled(); - expect(onCommandSpy).not.toHaveBeenCalled(); - }); - - it('should call internal event handler subscribe and expect the "onCommand" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onColumnsChanged'); - const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onBeforeMenuShow'); - const onAfterSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onAfterMenuShow'); - const onCloseSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onMenuClose'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'help' }, column: {} as Column, grid: gridStub, command: 'help' }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(5); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onCommandSpy).toHaveBeenCalledWith(expect.anything(), { item: { command: 'help' }, column: {} as Column, grid: gridStub, command: 'help' }); - expect(onAfterSpy).not.toHaveBeenCalled(); - expect(onColumnSpy).not.toHaveBeenCalled(); - expect(onBeforeSpy).not.toHaveBeenCalled(); - expect(onCloseSpy).not.toHaveBeenCalled(); - }); - - it('should call internal event handler subscribe and expect the "onAfterMenuShow" option to be called when addon notify is called', () => { - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onColumnsChanged'); - const onAfterSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onAfterMenuShow'); - const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onBeforeMenuShow'); - const onCloseSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onMenuClose'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - - const instance = extension.register() as SlickGridMenu; - instance.onAfterMenuShow!.notify({ columns: [], grid: gridStub, menu: divElement }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalledTimes(5); - expect(handlerSpy).toHaveBeenCalledWith( - { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, - expect.anything() - ); - expect(onAfterSpy).toHaveBeenCalledWith(expect.anything(), { columns: [], grid: gridStub, menu: divElement }); - expect(onBeforeSpy).not.toHaveBeenCalled(); - expect(onColumnSpy).not.toHaveBeenCalled(); - expect(onCloseSpy).not.toHaveBeenCalled(); - expect(onCommandSpy).not.toHaveBeenCalled(); - }); - - it('should call "autosizeColumns" method when the "onMenuClose" event was triggered and the columns are different', () => { - jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); - const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); - const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onColumnsChanged'); - const onCloseSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onMenuClose'); - const autoSizeSpy = jest.spyOn(gridStub, 'autosizeColumns'); - - const instance = extension.register() as SlickGridMenu; - instance!.onColumnsChanged!.notify({ columnId: 'field1', showing: true, grid: gridStub, allColumns: columnsMock, columns: columnsMock.slice(0, 1) }, new Slick.EventData(), gridStub); - instance.onMenuClose!.notify({ allColumns: columnsMock, visibleColumns: columnsMock, grid: gridStub, menu: divElement }, new Slick.EventData(), gridStub); - - expect(handlerSpy).toHaveBeenCalled(); - expect(onCloseSpy).toHaveBeenCalled(); - expect(onColumnSpy).toHaveBeenCalled(); - expect(autoSizeSpy).toHaveBeenCalled(); - }); - - it('should dispose of the addon', () => { - const instance = extension.register() as SlickGridMenu; - const destroySpy = jest.spyOn(instance, 'destroy'); - - extension.dispose(); - - expect(destroySpy).toHaveBeenCalled(); - }); - }); - - describe('addGridMenuCustomCommands method', () => { - afterEach(() => { - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - }); - - it('should expect an empty "customItems" array when both Filter & Sort are disabled', () => { - extension.register(); - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([]); - }); - - it('should expect menu related to "Unfreeze Columns/Rows"', () => { - const copyGridOptionsMock = { ...gridOptionsMock, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: false, } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - extension.register(); // calling 2x register to make sure it doesn't duplicate commands - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { iconCssClass: 'fa fa-times', title: 'Dégeler les colonnes/rangées', disabled: false, command: 'clear-pinning', positionOrder: 52 }, - ]); - }); - - it('should expect all menu related to Filter when "enableFilering" is set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: true, } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - extension.register(); // calling 2x register to make sure it doesn't duplicate commands - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { iconCssClass: 'fa fa-filter text-danger', title: 'Supprimer tous les filtres', disabled: false, command: 'clear-filter', positionOrder: 50 }, - { iconCssClass: 'fa fa-random', title: 'Basculer la ligne des filtres', disabled: false, command: 'toggle-filter', positionOrder: 53 }, - { iconCssClass: 'fa fa-refresh', title: 'Rafraîchir les données', disabled: false, command: 'refresh-dataset', positionOrder: 57 } - ]); - }); - - it('should have only 1 menu "clear-filter" when all other menus are defined as hidden & when "enableFilering" is set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideToggleFilterCommand: true, hideRefreshDatasetCommand: true } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - extension.register(); // calling 2x register to make sure it doesn't duplicate commands - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { iconCssClass: 'fa fa-filter text-danger', title: 'Supprimer tous les filtres', disabled: false, command: 'clear-filter', positionOrder: 50 } - ]); - }); - - it('should have only 1 menu "toggle-filter" when all other menus are defined as hidden & when "enableFilering" is set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideClearAllFiltersCommand: true, hideRefreshDatasetCommand: true } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - extension.register(); // calling 2x register to make sure it doesn't duplicate commands - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { iconCssClass: 'fa fa-random', title: 'Basculer la ligne des filtres', disabled: false, command: 'toggle-filter', positionOrder: 53 }, - ]); - }); - - it('should have only 1 menu "refresh-dataset" when all other menus are defined as hidden & when "enableFilering" is set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, showHeaderRow: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideClearAllFiltersCommand: true, hideToggleFilterCommand: true } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - extension.register(); // calling 2x register to make sure it doesn't duplicate commands - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { iconCssClass: 'fa fa-refresh', title: 'Rafraîchir les données', disabled: false, command: 'refresh-dataset', positionOrder: 57 } - ]); - }); - - it('should have the "toggle-preheader" menu command when "showPreHeaderPanel" is set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, showPreHeaderPanel: true } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - extension.register(); // calling 2x register to make sure it doesn't duplicate commands - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { iconCssClass: 'fa fa-random', title: 'Basculer la ligne de pré-en-tête', disabled: false, command: 'toggle-preheader', positionOrder: 53 } - ]); - }); - - it('should not have the "toggle-preheader" menu command when "showPreHeaderPanel" and "hideTogglePreHeaderCommand" are set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, showPreHeaderPanel: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideTogglePreHeaderCommand: true } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([]); - }); - - it('should have the "clear-sorting" menu command when "enableSorting" is set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableSorting: true } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - extension.register(); // calling 2x register to make sure it doesn't duplicate commands - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { iconCssClass: 'fa fa-unsorted text-danger', title: 'Supprimer tous les tris', disabled: false, command: 'clear-sorting', positionOrder: 51 } - ]); - }); - - it('should not have the "clear-sorting" menu command when "enableSorting" and "hideClearAllSortingCommand" are set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableSorting: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideClearAllSortingCommand: true } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([]); - }); - - it('should have the "export-csv" menu command when "enableTextExport" is set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideExportExcelCommand: true, hideExportTextDelimitedCommand: true } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - extension.register(); // calling 2x register to make sure it doesn't duplicate commands - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { iconCssClass: 'fa fa-download', title: 'Exporter en format CSV', disabled: false, command: 'export-csv', positionOrder: 54 } - ]); - }); - - it('should not have the "export-csv" menu command when "enableTextExport" and "hideExportCsvCommand" are set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideExportExcelCommand: true, hideExportCsvCommand: true, hideExportTextDelimitedCommand: true } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([]); - }); - - it('should have the "export-excel" menu command when "enableTextExport" is set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: true, enableTextExport: false, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideExportCsvCommand: true, hideExportExcelCommand: false } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - extension.register(); // calling 2x register to make sure it doesn't duplicate commands - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { iconCssClass: 'fa fa-file-excel-o text-success', title: 'Exporter vers Excel', disabled: false, command: 'export-excel', positionOrder: 55 } - ]); - }); - - it('should have the "export-text-delimited" menu command when "enableTextExport" is set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideExportCsvCommand: true, hideExportExcelCommand: true } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - extension.register(); // calling 2x register to make sure it doesn't duplicate commands - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { iconCssClass: 'fa fa-download', title: 'Exporter en format texte (délimité par tabulation)', disabled: false, command: 'export-text-delimited', positionOrder: 56 } - ]); - }); - - it('should not have the "export-text-delimited" menu command when "enableTextExport" and "hideExportCsvCommand" are set', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableTextExport: true, gridMenu: { commandLabels: gridOptionsMock.gridMenu.commandLabels, hideClearFrozenColumnsCommand: true, hideExportExcelCommand: true, hideExportCsvCommand: true, hideExportTextDelimitedCommand: true } } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - extension.register(); - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([]); - }); - }); - - describe('adding Grid Menu Custom Items', () => { - const customItemsMock = [{ - iconCssClass: 'fa fa-question-circle', - titleKey: 'HELP', - disabled: false, - command: 'help', - positionOrder: 99 - }]; - - beforeEach(() => { - const copyGridOptionsMock = { - ...gridOptionsMock, - enableTextExport: true, - gridMenu: { - commandLabels: gridOptionsMock.gridMenu.commandLabels, - customItems: customItemsMock, - hideClearFrozenColumnsCommand: true, - hideExportCsvCommand: false, - hideExportExcelCommand: false, - hideExportTextDelimitedCommand: true, - hideRefreshDatasetCommand: true, - hideSyncResizeButton: true, - hideToggleFilterCommand: true, - hideTogglePreHeaderCommand: true - } - } as unknown as GridOption; - - jest.spyOn(SharedService.prototype, 'dataView', 'get').mockReturnValue(dataViewStub); - jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(columnsMock); - jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(columnsMock); - jest.spyOn(SharedService.prototype, 'columnDefinitions', 'get').mockReturnValue(columnsMock); - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - }); - - afterEach(() => { - extension.dispose(); - }); - - it('should have user grid menu custom items', () => { - extension.register(); - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { command: 'export-csv', disabled: false, iconCssClass: 'fa fa-download', positionOrder: 54, title: 'Exporter en format CSV' }, - // { command: 'export-excel', disabled: false, iconCssClass: 'fa fa-file-excel-o text-success', positionOrder: 54, title: 'Exporter vers Excel' }, - { command: 'help', disabled: false, iconCssClass: 'fa fa-question-circle', positionOrder: 99, title: 'Aide', titleKey: 'HELP' }, - ]); - }); - - it('should have same user grid menu custom items even when grid menu extension is registered multiple times', () => { - extension.register(); - extension.register(); - expect(SharedService.prototype.gridOptions.gridMenu!.customItems).toEqual([ - { command: 'export-csv', disabled: false, iconCssClass: 'fa fa-download', positionOrder: 54, title: 'Exporter en format CSV' }, - // { command: 'export-excel', disabled: false, iconCssClass: 'fa fa-file-excel-o text-success', positionOrder: 54, title: 'Exporter vers Excel' }, - { command: 'help', disabled: false, iconCssClass: 'fa fa-question-circle', positionOrder: 99, title: 'Aide', titleKey: 'HELP' }, - ]); - }); - }); - - describe('refreshBackendDataset method', () => { - afterEach(() => { - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - }); - - it('should throw an error when backendServiceApi is not provided in the grid options', () => { - const copyGridOptionsMock = { ...gridOptionsMock, backendServiceApi: {} } as unknown as GridOption; - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); - expect(() => extension.refreshBackendDataset()).toThrowError(`BackendServiceApi requires at least a "process" function and a "service" defined`); - }); - - it('should call the backend service API to refresh the dataset', (done) => { - const now = new Date(); - const query = `query { users (first:20,offset:0) { totalCount, nodes { id,name,gender,company } } }`; - const processResult = { - data: { users: { nodes: [] }, pageInfo: { hasNextPage: true }, totalCount: 0 }, - metrics: { startTime: now, endTime: now, executionTime: 0, totalItemCount: 0 } - }; - const preSpy = jest.spyOn(gridOptionsMock.backendServiceApi as BackendServiceApi, 'preProcess'); - const postSpy = jest.spyOn(gridOptionsMock.backendServiceApi as BackendServiceApi, 'postProcess'); - const promise = new Promise((resolve) => setTimeout(() => resolve(processResult), 1)); - const processSpy = jest.spyOn(gridOptionsMock.backendServiceApi as BackendServiceApi, 'process').mockReturnValue(promise); - jest.spyOn(gridOptionsMock.backendServiceApi!.service, 'buildQuery').mockReturnValue(query); - - extension.refreshBackendDataset({ enableAddRow: true }); - - expect(preSpy).toHaveBeenCalled(); - expect(processSpy).toHaveBeenCalled(); - promise.then(() => { - expect(postSpy).toHaveBeenCalledWith(processResult); - done(); - }); - }); - }); - - describe('executeGridMenuInternalCustomCommands method', () => { - beforeEach(() => { - jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); - }); - - afterEach(() => { - jest.clearAllMocks(); - extension.eventHandler.unsubscribeAll(); - mockGridMenuAddon.onCommand = new Slick.Event(); - }); - - it('should call "clearFrozenColumns" when the command triggered is "clear-pinning"', () => { - const setOptionsSpy = jest.spyOn(gridStub, 'setOptions'); - const setColumnsSpy = jest.spyOn(gridStub, 'setColumns'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(columnsMock); - jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(columnsMock.slice(0, 1)); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'clear-pinning' }, column: {} as Column, grid: gridStub, command: 'clear-pinning' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(setColumnsSpy).toHaveBeenCalled(); - expect(setOptionsSpy).toHaveBeenCalledWith({ frozenColumn: -1, frozenRow: -1, frozenBottom: false, enableMouseWheelScrollHandler: false }); - }); - - it('should call "clearFilters" and dataview refresh when the command triggered is "clear-filter"', () => { - const filterSpy = jest.spyOn(filterServiceStub, 'clearFilters'); - const refreshSpy = jest.spyOn(SharedService.prototype.dataView, 'refresh'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'clear-filter' }, column: {} as Column, grid: gridStub, command: 'clear-filter' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(filterSpy).toHaveBeenCalled(); - expect(refreshSpy).toHaveBeenCalled(); - }); - - it('should call "clearSorting" and dataview refresh when the command triggered is "clear-sorting"', () => { - const sortSpy = jest.spyOn(sortServiceStub, 'clearSorting'); - const refreshSpy = jest.spyOn(SharedService.prototype.dataView, 'refresh'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'clear-sorting' }, column: {} as Column, grid: gridStub, command: 'clear-sorting' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(sortSpy).toHaveBeenCalled(); - expect(refreshSpy).toHaveBeenCalled(); - }); - - it('should call "exportToExcel" and expect an error thrown when ExcelExportService is not registered prior to calling the method', (done) => { - try { - jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([]); - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'export-excel' }, column: {} as Column, grid: gridStub, command: 'export-excel' }, new Slick.EventData(), gridStub); - } catch (e) { - expect(e.message).toContain('[Slickgrid-Universal] You must register the ExcelExportService to properly use Export to Excel in the Grid Menu.'); - done(); - } - }); - - it('should call "exportToFile" with CSV and expect an error thrown when TextExportService is not registered prior to calling the method', (done) => { - try { - jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([]); - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'export-csv' }, column: {} as Column, grid: gridStub, command: 'export-csv' }, new Slick.EventData(), gridStub); - } catch (e) { - expect(e.message).toContain('[Slickgrid-Universal] You must register the TextExportService to properly use Export to File in the Grid Menu.'); - done(); - } - }); - - it('should call "exportToFile" with Text Delimited and expect an error thrown when TextExportService is not registered prior to calling the method', (done) => { - try { - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'export-text-delimited' }, column: {} as Column, grid: gridStub, command: 'export-text-delimited' }, new Slick.EventData(), gridStub); - } catch (e) { - expect(e.message).toContain('[Slickgrid-Universal] You must register the TextExportService to properly use Export to File in the Grid Menu.'); - done(); - } - }); - - it('should call "exportToExcel" when the command triggered is "export-excel"', () => { - const excelExportSpy = jest.spyOn(excelExportServiceStub, 'exportToExcel'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([excelExportServiceStub]); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'export-excel' }, column: {} as Column, grid: gridStub, command: 'export-excel' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(excelExportSpy).toHaveBeenCalled(); - }); - - it('should call "exportToFile" with CSV set when the command triggered is "export-csv"', () => { - const exportSpy = jest.spyOn(exportServiceStub, 'exportToFile'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([exportServiceStub]); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'export-csv' }, column: {} as Column, grid: gridStub, command: 'export-csv' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(exportSpy).toHaveBeenCalledWith({ - delimiter: DelimiterType.comma, - format: FileType.csv, - }); - }); - - it('should call "exportToFile" with Text Delimited set when the command triggered is "export-text-delimited"', () => { - const exportSpy = jest.spyOn(exportServiceStub, 'exportToFile'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - jest.spyOn(SharedService.prototype, 'externalRegisteredResources', 'get').mockReturnValue([exportServiceStub]); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'export-text-delimited' }, column: {} as Column, grid: gridStub, command: 'export-text-delimited' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(exportSpy).toHaveBeenCalledWith({ - delimiter: DelimiterType.tab, - format: FileType.txt, - }); - }); - - it('should call the grid "setHeaderRowVisibility" method when the command triggered is "toggle-filter"', () => { - gridOptionsMock.showHeaderRow = false; - const setHeaderSpy = jest.spyOn(gridStub, 'setHeaderRowVisibility'); - const scrollSpy = jest.spyOn(gridStub, 'scrollColumnIntoView'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - const setColumnSpy = jest.spyOn(gridStub, 'setColumns'); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'toggle-filter' }, column: {} as Column, grid: gridStub, command: 'toggle-filter' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(setHeaderSpy).toHaveBeenCalledWith(true); - expect(scrollSpy).toHaveBeenCalledWith(0); - expect(setColumnSpy).toHaveBeenCalledTimes(1); - - gridOptionsMock.showHeaderRow = true; - instance.onCommand!.notify({ item: { command: 'toggle-filter' }, column: {} as Column, grid: gridStub, command: 'toggle-filter' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(setHeaderSpy).toHaveBeenCalledWith(false); - expect(setColumnSpy).toHaveBeenCalledTimes(1); // same as before, so count won't increase - }); - - it('should call the grid "setTopPanelVisibility" method when the command triggered is "toggle-toppanel"', () => { - gridOptionsMock.showTopPanel = false; - const gridSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setTopPanelVisibility'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'toggle-toppanel' }, column: {} as Column, grid: gridStub, command: 'toggle-toppanel' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(gridSpy).toHaveBeenCalledWith(true); - - gridOptionsMock.showTopPanel = true; - instance.onCommand!.notify({ item: { command: 'toggle-toppanel' }, column: {} as Column, grid: gridStub, command: 'toggle-toppanel' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(gridSpy).toHaveBeenCalledWith(false); - }); - - it('should call the grid "setPreHeaderPanelVisibility" method when the command triggered is "toggle-preheader"', () => { - gridOptionsMock.showPreHeaderPanel = false; - const gridSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setPreHeaderPanelVisibility'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'toggle-preheader' }, column: {} as Column, grid: gridStub, command: 'toggle-preheader' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(gridSpy).toHaveBeenCalledWith(true); - - gridOptionsMock.showPreHeaderPanel = true; - instance.onCommand!.notify({ item: { command: 'toggle-preheader' }, column: {} as Column, grid: gridStub, command: 'toggle-preheader' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(gridSpy).toHaveBeenCalledWith(false); - }); - - it('should call "refreshBackendDataset" method when the command triggered is "refresh-dataset"', () => { - const refreshSpy = jest.spyOn(extension, 'refreshBackendDataset'); - const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); - - const instance = extension.register() as SlickGridMenu; - instance.onCommand!.notify({ item: { command: 'refresh-dataset' }, column: {} as Column, grid: gridStub, command: 'refresh-dataset' }, new Slick.EventData(), gridStub); - - expect(onCommandSpy).toHaveBeenCalled(); - expect(refreshSpy).toHaveBeenCalled(); - }); - }); - - describe('translateGridMenu method', () => { - it('should translate the column picker header titles', () => { - const utilitySpy = jest.spyOn(extensionUtility, 'getPickerTitleOutputString'); - const translateSpy = jest.spyOn(extensionUtility, 'translateItems'); - - const instance = extension.register() as SlickGridMenu; - extension.translateGridMenu(); - const updateColsSpy = jest.spyOn(instance, 'updateAllTitles'); - - expect(utilitySpy).toHaveBeenCalled(); - expect(translateSpy).toHaveBeenCalled(); - expect(updateColsSpy).toHaveBeenCalledWith(SharedService.prototype.gridOptions.gridMenu); - expect(SharedService.prototype.gridOptions.gridMenu!.columnTitle).toBe('Colonnes'); - expect(SharedService.prototype.gridOptions.gridMenu!.forceFitTitle).toBe('Ajustement forcé des colonnes'); - expect(SharedService.prototype.gridOptions.gridMenu!.syncResizeTitle).toBe('Redimension synchrone'); - expect(columnsMock).toEqual([ - { id: 'field1', field: 'field1', width: 100, name: 'Titre', nameKey: 'TITLE' }, - { id: 'field2', field: 'field2', width: 75 } - ]); - }); - }); - - describe('showGridMenu method', () => { - it('should call the show grid menu', () => { - const instance = extension.register() as SlickGridMenu; - - const showSpy = jest.spyOn(instance, 'showGridMenu'); - extension.showGridMenu(null as any); - - expect(showSpy).toHaveBeenCalled(); - }); - }); - }); - - describe('without Translate Service', () => { - beforeEach(() => { - translateService = undefined as any; - backendUtilityService = new BackendUtilityService(); - extension = new GridMenuExtension({} as ExtensionUtility, filterServiceStub, { gridOptions: { enableTranslate: true } } as SharedService, {} as SortService, backendUtilityService, translateService); - }); - - it('should throw an error if "enableTranslate" is set but the I18N Service is null', () => { - expect(() => extension.register()).toThrowError('[Slickgrid-Universal] requires a Translate Service to be installed and configured'); - }); - }); -}); diff --git a/packages/common/src/extensions/__tests__/headerButtonExtension.spec.ts b/packages/common/src/extensions/__tests__/headerButtonExtension.spec.ts index 3befcb33e..b9358ff05 100644 --- a/packages/common/src/extensions/__tests__/headerButtonExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/headerButtonExtension.spec.ts @@ -3,6 +3,7 @@ import { ExtensionUtility } from '../extensionUtility'; import { SharedService } from '../../services/shared.service'; import { GridOption, HeaderButton, HeaderButtonOnCommandArgs, SlickGrid, SlickHeaderButtons, SlickNamespace } from '../../interfaces/index'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; +import { BackendUtilityService } from '../../services'; declare const Slick: SlickNamespace; @@ -22,6 +23,7 @@ describe('headerButtonExtension', () => { Slick.Plugins = { HeaderButtons: mockAddon } as any; let extension: HeaderButtonExtension; + let backendUtilityService: BackendUtilityService; let extensionUtility: ExtensionUtility; let sharedService: SharedService; let translateService: TranslateServiceStub; @@ -46,8 +48,9 @@ describe('headerButtonExtension', () => { beforeEach(() => { sharedService = new SharedService(); + backendUtilityService = new BackendUtilityService(); translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); extension = new HeaderButtonExtension(extensionUtility, sharedService); }); diff --git a/packages/common/src/extensions/__tests__/headerMenuExtension.spec.ts b/packages/common/src/extensions/__tests__/headerMenuExtension.spec.ts index 2c3ef39a6..7ade3bf65 100644 --- a/packages/common/src/extensions/__tests__/headerMenuExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/headerMenuExtension.spec.ts @@ -2,7 +2,7 @@ import { HeaderMenuExtension } from '../headerMenuExtension'; import { ExtensionUtility } from '../extensionUtility'; import { SharedService } from '../../services/shared.service'; import { Column, ColumnSort, SlickDataView, GridOption, SlickGrid, SlickNamespace, HeaderMenu, SlickHeaderMenu } from '../../interfaces/index'; -import { FilterService, SortService, PubSubService } from '../../services'; +import { FilterService, SortService, PubSubService, BackendUtilityService } from '../../services'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; declare const Slick: SlickNamespace; @@ -62,6 +62,7 @@ describe('headerMenuExtension', () => { const columnsMock: Column[] = [{ id: 'field1', field: 'field1', width: 100, nameKey: 'TITLE' }, { id: 'field2', field: 'field2', width: 75 }]; let extensionUtility: ExtensionUtility; + let backendUtilityService: BackendUtilityService; let extension: HeaderMenuExtension; let sharedService: SharedService; let translateService: TranslateServiceStub; @@ -104,8 +105,9 @@ describe('headerMenuExtension', () => { beforeEach(() => { divElement = document.createElement('div'); sharedService = new SharedService(); + backendUtilityService = new BackendUtilityService(); translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); extension = new HeaderMenuExtension(extensionUtility, filterServiceStub, pubSubServiceStub, sharedService, sortServiceStub, translateService); translateService.use('fr'); }); diff --git a/packages/common/src/extensions/__tests__/rowSelectionExtension.spec.ts b/packages/common/src/extensions/__tests__/rowSelectionExtension.spec.ts index add85d0ef..b07af8f2a 100644 --- a/packages/common/src/extensions/__tests__/rowSelectionExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/rowSelectionExtension.spec.ts @@ -3,6 +3,7 @@ import { RowSelectionExtension } from '../rowSelectionExtension'; import { ExtensionUtility } from '../extensionUtility'; import { SharedService } from '../../services/shared.service'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; +import { BackendUtilityService } from '../../services'; declare const Slick: SlickNamespace; @@ -24,14 +25,16 @@ describe('rowSelectionExtension', () => { let extension: RowSelectionExtension; let extensionUtility: ExtensionUtility; + let backendUtilityService: BackendUtilityService; let sharedService: SharedService; let translateService: TranslateServiceStub; const gridOptionsMock = { enableRowSelection: true } as GridOption; beforeEach(() => { sharedService = new SharedService(); + backendUtilityService = new BackendUtilityService(); translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); + extensionUtility = new ExtensionUtility(sharedService, backendUtilityService, translateService); extension = new RowSelectionExtension(sharedService); }); diff --git a/packages/common/src/extensions/extensionUtility.ts b/packages/common/src/extensions/extensionUtility.ts index 5d8c9f770..e64d78dbc 100644 --- a/packages/common/src/extensions/extensionUtility.ts +++ b/packages/common/src/extensions/extensionUtility.ts @@ -1,12 +1,18 @@ import { Constants } from '../constants'; import { Column } from '../interfaces/column.interface'; +import { BackendUtilityService } from '../services/backendUtility.service'; import { SharedService } from '../services/shared.service'; -import { TranslaterService } from '../services'; +import { TranslaterService } from '../services/translater.service'; import { getTranslationPrefix } from '../services/utilities'; import { Locale } from '../interfaces/locale.interface'; +import { GridOption } from '../interfaces'; export class ExtensionUtility { - constructor(private readonly sharedService: SharedService, private readonly translaterService?: TranslaterService) { } + constructor( + private readonly sharedService: SharedService, + private readonly backendUtilities?: BackendUtilityService, + private readonly translaterService?: TranslaterService + ) { } /** * From a Grid Menu object property name, we will return the correct title output string following this order @@ -93,6 +99,17 @@ export class ExtensionUtility { } } + /** Refresh the dataset through the Backend Service */ + refreshBackendDataset(inputGridOptions?: GridOption) { + // user can pass new set of grid options which will override current ones + let gridOptions = this.sharedService.gridOptions; + if (inputGridOptions) { + gridOptions = { ...this.sharedService.gridOptions, ...inputGridOptions }; + this.sharedService.gridOptions = gridOptions; + } + this.backendUtilities?.refreshBackendDataset(gridOptions); + } + /** * Sort items (by pointers) in an array by a property name * @param {Array} items array diff --git a/packages/common/src/extensions/gridMenuExtension.ts b/packages/common/src/extensions/gridMenuExtension.ts deleted file mode 100644 index 943c8f200..000000000 --- a/packages/common/src/extensions/gridMenuExtension.ts +++ /dev/null @@ -1,524 +0,0 @@ -import 'slickgrid/controls/slick.gridmenu'; - -import { - Extension, - GetSlickEventType, - GridOption, - GridMenu, - GridMenuItem, - SlickEventData, - SlickEventHandler, - SlickGridMenu, - SlickNamespace, -} from '../interfaces/index'; -import { DelimiterType, FileType } from '../enums/index'; -import { ExcelExportService } from '../services/excelExport.service'; -import { TextExportService } from '../services/textExport.service'; -import { ExtensionUtility } from './extensionUtility'; -import { FilterService } from '../services/filter.service'; -import { SortService } from '../services/sort.service'; -import { SharedService } from '../services/shared.service'; -import { TranslaterService } from '../services/translater.service'; -import { BackendUtilityService } from '../services/backendUtility.service'; -import { getTranslationPrefix } from '../services/utilities'; - -// using external js libraries -declare const Slick: SlickNamespace; - -export class GridMenuExtension implements Extension { - private _addon: SlickGridMenu | null = null; - private _areVisibleColumnDifferent = false; - private _eventHandler: SlickEventHandler; - private _gridMenuOptions: GridMenu | null = null; - private _userOriginalGridMenu!: GridMenu; - - constructor( - private readonly extensionUtility: ExtensionUtility, - private readonly filterService: FilterService, - private readonly sharedService: SharedService, - private readonly sortService: SortService, - private readonly backendUtilities?: BackendUtilityService, - private readonly translaterService?: TranslaterService, - ) { - this._eventHandler = new Slick.EventHandler(); - } - - get eventHandler(): SlickEventHandler { - return this._eventHandler; - } - - dispose() { - // unsubscribe all SlickGrid events - this._eventHandler.unsubscribeAll(); - if (this._addon?.destroy) { - this._addon.destroy(); - } - if (this.sharedService.gridOptions?.gridMenu?.customItems) { - this.sharedService.gridOptions.gridMenu = this._userOriginalGridMenu; - } - this.extensionUtility.nullifyFunctionNameStartingWithOn(this._gridMenuOptions); - this._addon = null; - this._gridMenuOptions = null; - } - - /** Get the instance of the SlickGrid addon (control or plugin). */ - getAddonInstance(): SlickGridMenu | null { - return this._addon; - } - - /** Register the 3rd party addon (plugin) */ - register(): SlickGridMenu | null { - if (this.sharedService.gridOptions?.enableTranslate && (!this.translaterService || !this.translaterService.translate)) { - throw new Error('[Slickgrid-Universal] requires a Translate Service to be installed and configured when the grid option "enableTranslate" is enabled.'); - } - - if (this.sharedService?.gridOptions?.gridMenu) { - // keep original user grid menu, useful when switching locale to translate - this._userOriginalGridMenu = { ...this.sharedService.gridOptions.gridMenu }; - - this._gridMenuOptions = { ...this.getDefaultGridMenuOptions(), ...this.sharedService.gridOptions.gridMenu }; - this.sharedService.gridOptions.gridMenu = this._gridMenuOptions; - - // merge original user grid menu items with internal items - // then sort all Grid Menu Custom Items (sorted by pointer, no need to use the return) - const originalCustomItems = this._userOriginalGridMenu && Array.isArray(this._userOriginalGridMenu.customItems) ? this._userOriginalGridMenu.customItems : []; - this._gridMenuOptions.customItems = [...originalCustomItems, ...this.addGridMenuCustomCommands(originalCustomItems)]; - this.extensionUtility.translateItems(this._gridMenuOptions.customItems, 'titleKey', 'title'); - this.extensionUtility.sortItems(this._gridMenuOptions.customItems, 'positionOrder'); - - this._addon = new Slick.Controls.GridMenu(this.sharedService.allColumns, this.sharedService.slickGrid, this.sharedService.gridOptions); - - // hook all events - if (this.sharedService.slickGrid && this._gridMenuOptions) { - if (this._gridMenuOptions.onExtensionRegistered) { - this._gridMenuOptions.onExtensionRegistered(this._addon); - } - - if (this._gridMenuOptions && typeof this._gridMenuOptions.onBeforeMenuShow === 'function') { - const onBeforeMenuShowHandler = this._addon.onBeforeMenuShow; - if (onBeforeMenuShowHandler) { - (this._eventHandler as SlickEventHandler>).subscribe(onBeforeMenuShowHandler, (e, args) => { - if (this._gridMenuOptions && this._gridMenuOptions.onBeforeMenuShow) { - this._gridMenuOptions.onBeforeMenuShow(e, args); - } - }); - } - } - - if (this._gridMenuOptions && typeof this._gridMenuOptions.onAfterMenuShow === 'function') { - const onAfterMenuShowHandler = this._addon.onAfterMenuShow; - if (onAfterMenuShowHandler) { - (this._eventHandler as SlickEventHandler>).subscribe(onAfterMenuShowHandler, (e, args) => { - if (this._gridMenuOptions && this._gridMenuOptions.onAfterMenuShow) { - this._gridMenuOptions.onAfterMenuShow(e, args); - } - }); - } - } - - const onColumnsChangedHandler = this._addon.onColumnsChanged; - if (onColumnsChangedHandler) { - (this._eventHandler as SlickEventHandler>).subscribe(onColumnsChangedHandler, (e, args) => { - this._areVisibleColumnDifferent = true; - if (this._gridMenuOptions && typeof this._gridMenuOptions.onColumnsChanged === 'function') { - this._gridMenuOptions.onColumnsChanged(e, args); - } - - // keep reference to the updated visible columns list - if (args && Array.isArray(args.columns) && args.columns.length > this.sharedService.visibleColumns.length) { - this.sharedService.visibleColumns = args.columns; - } - - // when using row selection, SlickGrid will only apply the "selected" CSS class on the visible columns only - // and if the row selection was done prior to the column being shown then that column that was previously hidden (at the time of the row selection) - // will not have the "selected" CSS class because it wasn't visible at the time. - // To bypass this problem we can simply recall the row selection with the same selection and that will trigger a re-apply of the CSS class - // on all columns including the column we just made visible - if (this.sharedService.gridOptions.enableRowSelection && args.showing) { - const rowSelection = args.grid.getSelectedRows(); - args.grid.setSelectedRows(rowSelection); - } - - // if we're using frozen columns, we need to readjust pinning when the new hidden column becomes visible again on the left pinning container - // we need to readjust frozenColumn index because SlickGrid freezes by index and has no knowledge of the columns themselves - const frozenColumnIndex = this.sharedService.gridOptions.frozenColumn ?? -1; - if (frozenColumnIndex >= 0) { - const { allColumns, columns: visibleColumns } = args; - this.extensionUtility.readjustFrozenColumnIndexWhenNeeded(frozenColumnIndex, allColumns, visibleColumns); - } - }); - } - - const onCommandHandler = this._addon.onCommand; - if (onCommandHandler) { - (this._eventHandler as SlickEventHandler>).subscribe(onCommandHandler, (e, args) => { - this.executeGridMenuInternalCustomCommands(e, args); - if (this._gridMenuOptions && typeof this._gridMenuOptions.onCommand === 'function') { - this._gridMenuOptions.onCommand(e, args); - } - }); - } - const onMenuCloseHandler = this._addon.onMenuClose; - if (onMenuCloseHandler) { - (this._eventHandler as SlickEventHandler>).subscribe(onMenuCloseHandler, (e, args) => { - if (this._gridMenuOptions && typeof this._gridMenuOptions.onMenuClose === 'function') { - this._gridMenuOptions.onMenuClose(e, args); - } - - // we also want to resize the columns if the user decided to hide certain column(s) - if (this.sharedService.slickGrid && typeof this.sharedService.slickGrid.autosizeColumns === 'function') { - // make sure that the grid still exist (by looking if the Grid UID is found in the DOM tree) - const gridUid = this.sharedService.slickGrid.getUID(); - if (this._areVisibleColumnDifferent && gridUid && document.querySelector(`.${gridUid}`) !== null) { - const gridOptions = this.sharedService.slickGrid.getOptions(); - if (gridOptions.enableAutoSizeColumns) { - this.sharedService.slickGrid.autosizeColumns(); - } - this._areVisibleColumnDifferent = false; - } - } - }); - } - } - return this._addon; - } - return null; - } - - /** Refresh the dataset through the Backend Service */ - refreshBackendDataset(gridOptions?: GridOption) { - // user can pass new set of grid options which will override current ones - if (gridOptions) { - this.sharedService.gridOptions = { ...this.sharedService.gridOptions, ...gridOptions }; - } - this.backendUtilities?.refreshBackendDataset(this.sharedService.gridOptions); - } - - showGridMenu(e: SlickEventData) { - if (this._addon) { - this._addon.showGridMenu(e); - } - } - - /** Translate the Grid Menu titles and column picker */ - translateGridMenu() { - // update the properties by pointers, that is the only way to get Grid Menu Control to see the new values - // we also need to call the control init so that it takes the new Grid object with latest values - if (this.sharedService?.gridOptions?.gridMenu) { - this.sharedService.gridOptions.gridMenu.customItems = []; - this.emptyGridMenuTitles(); - - // merge original user grid menu items with internal items - // then sort all Grid Menu Custom Items (sorted by pointer, no need to use the return) - const originalCustomItems = this._userOriginalGridMenu && Array.isArray(this._userOriginalGridMenu.customItems) ? this._userOriginalGridMenu.customItems : []; - this.sharedService.gridOptions.gridMenu.customItems = [...originalCustomItems, ...this.addGridMenuCustomCommands(originalCustomItems)]; - this.extensionUtility.translateItems(this.sharedService.gridOptions.gridMenu.customItems, 'titleKey', 'title'); - this.extensionUtility.sortItems(this.sharedService.gridOptions.gridMenu.customItems, 'positionOrder'); - - this.sharedService.gridOptions.gridMenu.columnTitle = this.extensionUtility.getPickerTitleOutputString('columnTitle', 'gridMenu'); - this.sharedService.gridOptions.gridMenu.forceFitTitle = this.extensionUtility.getPickerTitleOutputString('forceFitTitle', 'gridMenu'); - this.sharedService.gridOptions.gridMenu.syncResizeTitle = this.extensionUtility.getPickerTitleOutputString('syncResizeTitle', 'gridMenu'); - - // translate all columns (including non-visible) - this.extensionUtility.translateItems(this.sharedService.allColumns, 'nameKey', 'name'); - - // update the Titles of each sections (command, customTitle, ...) - if (this._addon?.updateAllTitles) { - this._addon.updateAllTitles(this.sharedService.gridOptions.gridMenu); - } - } - } - - // -- - // private functions - // ------------------ - - /** Create Grid Menu with Custom Commands if user has enabled Filters and/or uses a Backend Service (OData, GraphQL) */ - private addGridMenuCustomCommands(originalCustomItems: Array) { - const backendApi = this.sharedService.gridOptions.backendServiceApi || null; - const gridMenuCustomItems: Array = []; - const gridOptions = this.sharedService.gridOptions; - const translationPrefix = getTranslationPrefix(gridOptions); - const commandLabels = this._gridMenuOptions?.commandLabels; - - // show grid menu: Unfreeze Columns/Rows - if (this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideClearFrozenColumnsCommand) { - const commandName = 'clear-pinning'; - if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { - gridMenuCustomItems.push( - { - iconCssClass: this._gridMenuOptions.iconClearFrozenColumnsCommand || 'fa fa-times', - title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.clearFrozenColumnsCommandKey}`, 'TEXT_CLEAR_PINNING', commandLabels?.clearFrozenColumnsCommand), - disabled: false, - command: commandName, - positionOrder: 52 - } - ); - } - } - - if (this.sharedService.gridOptions && (this.sharedService.gridOptions.enableFiltering && !this.sharedService.hideHeaderRowAfterPageLoad)) { - // show grid menu: Clear all Filters - if (this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideClearAllFiltersCommand) { - const commandName = 'clear-filter'; - if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { - gridMenuCustomItems.push( - { - iconCssClass: this._gridMenuOptions.iconClearAllFiltersCommand || 'fa fa-filter text-danger', - title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.clearAllFiltersCommandKey}`, 'TEXT_CLEAR_ALL_FILTERS', commandLabels?.clearAllFiltersCommand), - disabled: false, - command: commandName, - positionOrder: 50 - } - ); - } - } - - // show grid menu: toggle filter row - if (this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideToggleFilterCommand) { - const commandName = 'toggle-filter'; - if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { - gridMenuCustomItems.push( - { - iconCssClass: this._gridMenuOptions.iconToggleFilterCommand || 'fa fa-random', - title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.toggleFilterCommandKey}`, 'TEXT_TOGGLE_FILTER_ROW', commandLabels?.toggleFilterCommand), - disabled: false, - command: commandName, - positionOrder: 53 - } - ); - } - } - - // show grid menu: refresh dataset - if (backendApi && this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideRefreshDatasetCommand) { - const commandName = 'refresh-dataset'; - if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { - gridMenuCustomItems.push( - { - iconCssClass: this._gridMenuOptions.iconRefreshDatasetCommand || 'fa fa-refresh', - title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.refreshDatasetCommandKey}`, 'TEXT_REFRESH_DATASET', commandLabels?.refreshDatasetCommand), - disabled: false, - command: commandName, - positionOrder: 57 - } - ); - } - } - } - - if (this.sharedService.gridOptions.showPreHeaderPanel) { - // show grid menu: toggle pre-header row - if (this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideTogglePreHeaderCommand) { - const commandName = 'toggle-preheader'; - if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { - gridMenuCustomItems.push( - { - iconCssClass: this._gridMenuOptions.iconTogglePreHeaderCommand || 'fa fa-random', - title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.togglePreHeaderCommandKey}`, 'TEXT_TOGGLE_PRE_HEADER_ROW', commandLabels?.togglePreHeaderCommand), - disabled: false, - command: commandName, - positionOrder: 53 - } - ); - } - } - } - - if (this.sharedService.gridOptions.enableSorting) { - // show grid menu: Clear all Sorting - if (this.sharedService.gridOptions && this._gridMenuOptions && !this._gridMenuOptions.hideClearAllSortingCommand) { - const commandName = 'clear-sorting'; - if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { - gridMenuCustomItems.push( - { - iconCssClass: this._gridMenuOptions.iconClearAllSortingCommand || 'fa fa-unsorted text-danger', - title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.clearAllSortingCommandKey}`, 'TEXT_CLEAR_ALL_SORTING', commandLabels?.clearAllSortingCommand), - disabled: false, - command: commandName, - positionOrder: 51 - } - ); - } - } - } - - // show grid menu: Export to file - if ((this.sharedService.gridOptions?.enableExport || this.sharedService.gridOptions?.enableTextExport) && this._gridMenuOptions && !this._gridMenuOptions.hideExportCsvCommand) { - const commandName = 'export-csv'; - if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { - gridMenuCustomItems.push( - { - iconCssClass: this._gridMenuOptions.iconExportCsvCommand || 'fa fa-download', - title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.exportCsvCommandKey}`, 'TEXT_EXPORT_TO_CSV', commandLabels?.exportCsvCommand), - disabled: false, - command: commandName, - positionOrder: 54 - } - ); - } - } - - // show grid menu: Export to Excel - if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableExcelExport && this._gridMenuOptions && !this._gridMenuOptions.hideExportExcelCommand) { - const commandName = 'export-excel'; - if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { - gridMenuCustomItems.push( - { - iconCssClass: this._gridMenuOptions.iconExportExcelCommand || 'fa fa-file-excel-o text-success', - title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.exportExcelCommandKey}`, 'TEXT_EXPORT_TO_EXCEL', commandLabels?.exportExcelCommand), - disabled: false, - command: commandName, - positionOrder: 55 - } - ); - } - } - - // show grid menu: export to text file as tab delimited - if ((this.sharedService.gridOptions?.enableExport || this.sharedService.gridOptions?.enableTextExport) && this._gridMenuOptions && !this._gridMenuOptions.hideExportTextDelimitedCommand) { - const commandName = 'export-text-delimited'; - if (!originalCustomItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { - gridMenuCustomItems.push( - { - iconCssClass: this._gridMenuOptions.iconExportTextDelimitedCommand || 'fa fa-download', - title: this.extensionUtility.translateWhenEnabledAndServiceExist(`${translationPrefix}${commandLabels?.exportTextDelimitedCommandKey}`, 'TEXT_EXPORT_TO_TAB_DELIMITED', commandLabels?.exportTextDelimitedCommand), - disabled: false, - command: commandName, - positionOrder: 56 - } - ); - } - } - - // add the custom "Commands" title if there are any commands - if (this.sharedService && this.sharedService.gridOptions && this._gridMenuOptions && (Array.isArray(gridMenuCustomItems) && gridMenuCustomItems.length > 0 || (Array.isArray(this._gridMenuOptions.customItems) && this._gridMenuOptions.customItems.length > 0))) { - this._gridMenuOptions.customTitle = this._gridMenuOptions.customTitle || this.extensionUtility.getPickerTitleOutputString('customTitle', 'gridMenu'); - } - - return gridMenuCustomItems; - } - - /** - * Execute the Grid Menu Custom command callback that was triggered by the onCommand subscribe - * These are the default internal custom commands - * @param event - * @param GridMenuItem args - */ - private executeGridMenuInternalCustomCommands(_e: Event, args: GridMenuItem) { - const registeredResources = this.sharedService?.externalRegisteredResources || []; - - if (args && args.command) { - switch (args.command) { - case 'clear-pinning': - const visibleColumns = [...this.sharedService.visibleColumns]; - const newGridOptions = { frozenColumn: -1, frozenRow: -1, frozenBottom: false, enableMouseWheelScrollHandler: false }; - this.sharedService.slickGrid.setOptions(newGridOptions); - this.sharedService.gridOptions.frozenColumn = newGridOptions.frozenColumn; - this.sharedService.gridOptions.frozenRow = newGridOptions.frozenRow; - this.sharedService.gridOptions.frozenBottom = newGridOptions.frozenBottom; - this.sharedService.gridOptions.enableMouseWheelScrollHandler = newGridOptions.enableMouseWheelScrollHandler; - - // SlickGrid seems to be somehow resetting the columns to their original positions, - // so let's re-fix them to the position we kept as reference - if (Array.isArray(visibleColumns)) { - this.sharedService.slickGrid.setColumns(visibleColumns); - } - - // we also need to autosize columns if the option is enabled - const gridOptions = this.sharedService.slickGrid.getOptions(); - if (gridOptions.enableAutoSizeColumns) { - this.sharedService.slickGrid.autosizeColumns(); - } - break; - case 'clear-filter': - this.filterService.clearFilters(); - this.sharedService.dataView.refresh(); - break; - case 'clear-sorting': - this.sortService.clearSorting(); - this.sharedService.dataView.refresh(); - break; - case 'export-csv': - const exportCsvService: TextExportService = registeredResources.find((service: any) => service.className === 'TextExportService'); - if (exportCsvService?.exportToFile) { - exportCsvService.exportToFile({ - delimiter: DelimiterType.comma, - format: FileType.csv, - }); - } else { - throw new Error(`[Slickgrid-Universal] You must register the TextExportService to properly use Export to File in the Grid Menu. Example:: this.gridOptions = { enableTextExport: true, registerExternalResources: [new TextExportService()] };`); - } - break; - case 'export-excel': - const excelService: ExcelExportService = registeredResources.find((service: any) => service.className === 'ExcelExportService'); - if (excelService?.exportToExcel) { - excelService.exportToExcel(); - } else { - throw new Error(`[Slickgrid-Universal] You must register the ExcelExportService to properly use Export to Excel in the Grid Menu. Example:: this.gridOptions = { enableExcelExport: true, registerExternalResources: [new ExcelExportService()] };`); - } - break; - case 'export-text-delimited': - const exportTxtService: TextExportService = registeredResources.find((service: any) => service.className === 'TextExportService'); - if (exportTxtService?.exportToFile) { - exportTxtService.exportToFile({ - delimiter: DelimiterType.tab, - format: FileType.txt, - }); - } else { - throw new Error(`[Slickgrid-Universal] You must register the TextExportService to properly use Export to File in the Grid Menu. Example:: this.gridOptions = { enableTextExport: true, registerExternalResources: [new TextExportService()] };`); - } - break; - case 'toggle-filter': - let showHeaderRow = this.sharedService && this.sharedService.gridOptions && this.sharedService.gridOptions.showHeaderRow || false; - showHeaderRow = !showHeaderRow; // inverse show header flag - this.sharedService.slickGrid.setHeaderRowVisibility(showHeaderRow); - - // when displaying header row, we'll call "setColumns" which in terms will recreate the header row filters - if (showHeaderRow === true) { - this.sharedService.slickGrid.setColumns(this.sharedService.columnDefinitions); - this.sharedService.slickGrid.scrollColumnIntoView(0); // quick fix to avoid filter being out of sync with horizontal scroll - } - break; - case 'toggle-toppanel': - const showTopPanel = this.sharedService && this.sharedService.gridOptions && this.sharedService.gridOptions.showTopPanel || false; - this.sharedService.slickGrid.setTopPanelVisibility(!showTopPanel); - break; - case 'toggle-preheader': - const showPreHeaderPanel = this.sharedService && this.sharedService.gridOptions && this.sharedService.gridOptions.showPreHeaderPanel || false; - this.sharedService.slickGrid.setPreHeaderPanelVisibility(!showPreHeaderPanel); - break; - case 'refresh-dataset': - this.refreshBackendDataset(); - break; - default: - break; - } - } - } - - private emptyGridMenuTitles() { - if (this.sharedService?.gridOptions?.gridMenu) { - this.sharedService.gridOptions.gridMenu.customTitle = ''; - this.sharedService.gridOptions.gridMenu.columnTitle = ''; - this.sharedService.gridOptions.gridMenu.forceFitTitle = ''; - this.sharedService.gridOptions.gridMenu.syncResizeTitle = ''; - } - } - - /** @return default Grid Menu options */ - private getDefaultGridMenuOptions(): GridMenu { - return { - customTitle: undefined, - columnTitle: this.extensionUtility.getPickerTitleOutputString('columnTitle', 'gridMenu'), - forceFitTitle: this.extensionUtility.getPickerTitleOutputString('forceFitTitle', 'gridMenu'), - syncResizeTitle: this.extensionUtility.getPickerTitleOutputString('syncResizeTitle', 'gridMenu'), - iconCssClass: 'fa fa-bars', - menuWidth: 18, - customItems: [], - hideClearAllFiltersCommand: false, - hideRefreshDatasetCommand: false, - hideToggleFilterCommand: false, - }; - } -} diff --git a/packages/common/src/extensions/index.ts b/packages/common/src/extensions/index.ts index 719ab7013..f8df6cb6f 100644 --- a/packages/common/src/extensions/index.ts +++ b/packages/common/src/extensions/index.ts @@ -4,7 +4,6 @@ export * from './checkboxSelectorExtension'; export * from './contextMenuExtension'; export * from './draggableGroupingExtension'; export * from './extensionUtility'; -export * from './gridMenuExtension'; export * from './groupItemMetaProviderExtension'; export * from './headerButtonExtension'; export * from './headerMenuExtension'; diff --git a/packages/common/src/global-grid-options.ts b/packages/common/src/global-grid-options.ts index 6ae7618ea..c51e55287 100644 --- a/packages/common/src/global-grid-options.ts +++ b/packages/common/src/global-grid-options.ts @@ -161,6 +161,7 @@ export const GlobalGridOptions: GridOption = { forceFitColumns: false, frozenHeaderWidthCalcDifferential: 1, gridMenu: { + alignDropSide: 'right', commandLabels: { clearAllFiltersCommandKey: 'CLEAR_ALL_FILTERS', clearAllSortingCommandKey: 'CLEAR_ALL_SORTING', @@ -195,7 +196,6 @@ export const GlobalGridOptions: GridOption = { iconTogglePreHeaderCommand: 'fa fa-random mdi mdi-flip-vertical', menuWidth: 16, resizeOnShowHeaderRow: true, - useClickToRepositionMenu: false, // use icon location to reposition instead headerColumnValueExtractor: pickerHeaderColumnValueExtractor }, headerMenu: { diff --git a/packages/common/src/interfaces/columnPicker.interface.ts b/packages/common/src/interfaces/columnPicker.interface.ts index e38b20728..06f968a69 100644 --- a/packages/common/src/interfaces/columnPicker.interface.ts +++ b/packages/common/src/interfaces/columnPicker.interface.ts @@ -1,4 +1,4 @@ -import { Column, GridOption } from './index'; +import { Column, GridOption, SlickGrid } from './index'; import { ColumnPickerControl } from '../controls/columnPicker.control'; export interface ColumnPicker extends ColumnPickerOption { @@ -36,5 +36,23 @@ export interface ColumnPickerOption { // Events /** SlickGrid Event fired when any of the columns checkbox selection changes. */ - onColumnsChanged?: (e: Event, args: any) => void; + onColumnsChanged?: (e: Event, args: { + /** column definition id */ + columnId: string; + + /** last command, are we showing or not the column? */ + showing: boolean; + + /** slick grid object */ + grid: SlickGrid; + + /** list of all column definitions (visible & hidden) */ + allColumns: Column[]; + + /** list of visible column definitions */ + visibleColumns: Column[]; + + /** @deprecated @use `visibleColumns` */ + columns?: Column[]; + }) => void; } diff --git a/packages/common/src/interfaces/draggableGroupingOption.interface.ts b/packages/common/src/interfaces/draggableGroupingOption.interface.ts index 0a6a8d8fa..384232873 100644 --- a/packages/common/src/interfaces/draggableGroupingOption.interface.ts +++ b/packages/common/src/interfaces/draggableGroupingOption.interface.ts @@ -2,10 +2,13 @@ import { ColumnReorderFunction } from '../enums/columnReorderFunction.type'; import { GroupingGetterFunction } from './grouping.interface'; export interface DraggableGroupingOption { - /** an extra CSS class to add to the delete button (default undefined), if deleteIconCssClass && deleteIconImage undefined then slick-groupby-remove-image class will be added */ + /** an extra CSS class to add to the delete button (default undefined), if deleteIconCssClass is undefined then slick-groupby-remove-image class will be added */ deleteIconCssClass?: string; - /** a url to the delete button image (default undefined) */ + /** + * @deprecated @use `deleteIconCssClass` + * a url to the delete button image (default undefined) + */ deleteIconImage?: string; /** option to specify set own placeholder note text */ @@ -14,7 +17,10 @@ export interface DraggableGroupingOption { /** an extra CSS class to add to the grouping field hint (default undefined) */ groupIconCssClass?: string; - /** a url to the grouping field hint image (default undefined) */ + /** + * @deprecated @use `groupIconCssClass` + * a url to the grouping field hint image (default undefined) + */ groupIconImage?: string; // diff --git a/packages/common/src/interfaces/gridMenu.interface.ts b/packages/common/src/interfaces/gridMenu.interface.ts index 84b3857f8..73e156888 100644 --- a/packages/common/src/interfaces/gridMenu.interface.ts +++ b/packages/common/src/interfaces/gridMenu.interface.ts @@ -1,31 +1,57 @@ import { Column, GridMenuOption, - MenuCommandItemCallbackArgs, + GridMenuCommandItemCallbackArgs, SlickGrid, - SlickGridMenu, - SlickEventData, } from './index'; +import { GridMenuControl } from '../controls/gridMenu.control'; export interface GridMenu extends GridMenuOption { // -- // Events /** Fired after extension (control) is registered by SlickGrid */ - onExtensionRegistered?: (plugin: SlickGridMenu) => void; + onExtensionRegistered?: (plugin: GridMenuControl) => void; - /** SlickGrid Event fired After the menu is shown. */ - onAfterMenuShow?: (e: SlickEventData, args: { grid: SlickGrid; menu: HTMLElement; columns: Column[] }) => void; + /** Callback fired After the menu is shown. */ + onAfterMenuShow?: (e: Event, args: GridMenuEventWithElementCallbackArgs) => boolean | void; - /** SlickGrid Event fired Before the menu is shown. */ - onBeforeMenuShow?: (e: SlickEventData, args: { grid: SlickGrid; menu: HTMLElement; columns: Column[] }) => void; + /** Callback fired Before the menu is shown. */ + onBeforeMenuShow?: (e: Event, args: GridMenuEventWithElementCallbackArgs) => boolean | void; - /** SlickGrid Event fired when any of the columns checkbox selection changes. */ - onColumnsChanged?: (e: SlickEventData, args: { grid: SlickGrid; allColumns: Column[]; columns: Column[]; }) => void; + /** Callback fired when any of the columns checkbox selection changes. */ + onColumnsChanged?: (e: Event, args: GridMenuOnColumnsChangedCallbackArgs) => void; - /** SlickGrid Event fired when the menu is closing. */ - onMenuClose?: (e: SlickEventData, args: { grid: SlickGrid; menu: HTMLElement; allColumns: Column[], visibleColumns: Column[] }) => void; + /** Callback fired when the menu is closing. */ + onMenuClose?: (e: Event, args: GridMenuEventWithElementCallbackArgs) => boolean | void; - /** SlickGrid Event fired on menu option clicked from the Command items list */ - onCommand?: (e: SlickEventData, args: MenuCommandItemCallbackArgs) => void; + /** Callback fired on menu option clicked from the Command items list */ + onCommand?: (e: Event, args: GridMenuCommandItemCallbackArgs) => void; } + +export interface GridMenuEventBaseCallbackArgs { + /** list of all column definitions (visible & hidden) */ + allColumns: Column[]; + + /** list of visible column definitions */ + visibleColumns: Column[]; + + /** slick grid object */ + grid: SlickGrid; +} + +export interface GridMenuEventWithElementCallbackArgs extends GridMenuEventBaseCallbackArgs { + /** html DOM element of the menu */ + menu: HTMLElement; +} + +export interface GridMenuOnColumnsChangedCallbackArgs extends GridMenuEventBaseCallbackArgs { + /** column definition id */ + columnId: string; + + /** last command, are we showing or not the column? */ + showing: boolean; + + /** @deprecated @use `visibleColumns` */ + columns?: Column[]; +} \ No newline at end of file diff --git a/packages/common/src/interfaces/gridMenuCommandItemCallbackArgs.interface.ts b/packages/common/src/interfaces/gridMenuCommandItemCallbackArgs.interface.ts new file mode 100644 index 000000000..90709292f --- /dev/null +++ b/packages/common/src/interfaces/gridMenuCommandItemCallbackArgs.interface.ts @@ -0,0 +1,19 @@ +import { Column, SlickGrid } from '.'; +import { MenuCommandItem } from './menuCommandItem.interface'; + +export interface GridMenuCommandItemCallbackArgs { + /** A command identifier returned by the onCommand (or action) event callback handler. */ + command: string; + + /** Menu item selected */ + item: MenuCommandItem; + + /** Slick Grid object */ + grid: SlickGrid; + + /** all columns (including hidden ones) */ + allColumns: Column[], + + /** only visible columns (excluding hidden columns) */ + visibleColumns: Column[], +} diff --git a/packages/common/src/interfaces/gridMenuItem.interface.ts b/packages/common/src/interfaces/gridMenuItem.interface.ts index 52634cc09..112a11a01 100644 --- a/packages/common/src/interfaces/gridMenuItem.interface.ts +++ b/packages/common/src/interfaces/gridMenuItem.interface.ts @@ -1,4 +1,5 @@ import { Column } from './column.interface'; +import { GridMenuCommandItemCallbackArgs } from './gridMenuCommandItemCallbackArgs.interface'; import { SlickGrid } from './slickGrid.interface'; export interface GridMenuItem { @@ -20,7 +21,10 @@ export interface GridMenuItem { /** CSS class to be added to the menu item icon. */ iconCssClass?: string; - /** URL pointing to the icon image. */ + /** + * @deprecated @use `iconCssClass` + * URL pointing to the icon image. + */ iconImage?: string; /** position order in the list, a lower number will make it on top of the list. Internal commands starts at 50. */ @@ -42,7 +46,7 @@ export interface GridMenuItem { // action/override callbacks /** Optionally define a callback function that gets executed when item is chosen (and/or use the onCommand event) */ - action?: (event: Event, callbackArgs: { command: string; grid: SlickGrid; menu: any; columns: Column[]; visibleColumns: Column[] }) => void; + action?: (event: Event, callbackArgs: GridMenuCommandItemCallbackArgs) => void; /** Callback method that user can override the default behavior of showing/hiding an item from the list. */ itemVisibilityOverride?: (args: { grid: SlickGrid; menu: any; columns: Column[]; visibleColumns: Column[] }) => boolean; diff --git a/packages/common/src/interfaces/gridMenuOption.interface.ts b/packages/common/src/interfaces/gridMenuOption.interface.ts index 109200365..07b99b125 100644 --- a/packages/common/src/interfaces/gridMenuOption.interface.ts +++ b/packages/common/src/interfaces/gridMenuOption.interface.ts @@ -1,6 +1,9 @@ import { Column, GridMenuItem, GridMenuLabel, GridOption, MenuCallbackArgs, } from './index'; export interface GridMenuOption { + /** Defaults to "right", which side to align the grid menu dropdown? */ + alignDropSide?: 'left' | 'right'; + /** * All the commands text labels * NOTE: some of the text have other properties outside of this option (like 'customTitle', 'forceFitTitle', ...) and that is because they were created prior to this refactoring of labels @@ -67,7 +70,7 @@ export interface GridMenuOption { /** Defaults to true, which will hide the "Toggle Pre-Header Row" (used by draggable grouping) command in the Grid Menu (Grid Option "showPreHeaderPanel: true" has to be enabled) */ hideTogglePreHeaderCommand?: boolean; - /** CSS class for the displaying the Grid menu icon image (basically the hamburger menu) */ + /** CSS class for the displaying the Grid menu icon (basically the hamburger menu) */ iconCssClass?: string; /** icon for the "Clear all Filters" command */ @@ -88,7 +91,10 @@ export interface GridMenuOption { /** icon for the "Export to Text Delimited" command */ iconExportTextDelimitedCommand?: string; - /** Link for the displaying the Grid menu icon image (basically the hamburger menu) */ + /** + * @deprecated @use `iconCssClass` + * URL pointing to the displaying the Grid menu icon image (basically the hamburger menu). + */ iconImage?: string; /** icon for the "Refresh Dataset" command */ @@ -121,9 +127,6 @@ export interface GridMenuOption { /** Same as "syncResizeTitle", except that it's a translation key which can be used on page load and/or when switching locale */ syncResizeTitleKey?: string; - /** Defaults to true, Use the Click offset to reposition the Grid Menu, when set to False it will use the icon offset to reposition the grid menu */ - useClickToRepositionMenu?: boolean; - // -- // action/override callbacks diff --git a/packages/common/src/interfaces/headerButtonItem.interface.ts b/packages/common/src/interfaces/headerButtonItem.interface.ts index 2e52df52a..5bb017bb3 100644 --- a/packages/common/src/interfaces/headerButtonItem.interface.ts +++ b/packages/common/src/interfaces/headerButtonItem.interface.ts @@ -11,7 +11,10 @@ export interface HeaderButtonItem { /** Button click handler. */ handler?: (e: Event) => void; - /** Relative button image path. */ + /** + * @deprecated @use `cssClass` + * Relative button image path. + */ image?: string; /** Only show the button on hover. */ diff --git a/packages/common/src/interfaces/headerMenuOption.interface.ts b/packages/common/src/interfaces/headerMenuOption.interface.ts index a15b8061a..e4ad2dfb8 100644 --- a/packages/common/src/interfaces/headerMenuOption.interface.ts +++ b/packages/common/src/interfaces/headerMenuOption.interface.ts @@ -11,7 +11,10 @@ export interface HeaderMenuOption { /** an extra CSS class to add to the menu button */ buttonCssClass?: string; - /** a url to the menu button image */ + /** + * @deprecated @use `buttonCssClass` + * URL pointing to the Header Menu button image. + */ buttonImage?: string; /** A command identifier to be passed to the onCommand event handlers. */ @@ -47,7 +50,10 @@ export interface HeaderMenuOption { /** A CSS class to be added to the menu item icon. */ iconCssClass?: string; - /** A url to the icon image. */ + /** + * @deprecated @use `iconCssClass` + * URL pointing to the Header Menu icon image. + */ iconImage?: string; /** icon for the "Remove Filter" command */ diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 744d06e4c..478224025 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -71,6 +71,7 @@ export * from './formatter.interface'; export * from './formatterOption.interface'; export * from './formatterResultObject.interface'; export * from './gridMenu.interface'; +export * from './gridMenuCommandItemCallbackArgs.interface'; export * from './gridMenuItem.interface'; export * from './gridMenuLabel.interface'; export * from './gridMenuOption.interface'; diff --git a/packages/common/src/interfaces/menuItem.interface.ts b/packages/common/src/interfaces/menuItem.interface.ts index 7f5660e2c..34ed6c2d2 100644 --- a/packages/common/src/interfaces/menuItem.interface.ts +++ b/packages/common/src/interfaces/menuItem.interface.ts @@ -16,7 +16,10 @@ export interface MenuItem { /** CSS class to be added to the menu item icon. */ iconCssClass?: string; - /** URL pointing to the icon image. */ + /** + * @deprecated @use `iconCssClass` + * URL pointing to the Menu icon image. + */ iconImage?: string; /** position order in the list, a lower number will make it on top of the list. Internal commands starts at 50. */ diff --git a/packages/common/src/services/__tests__/extension.service.spec.ts b/packages/common/src/services/__tests__/extension.service.spec.ts index 8428e39e6..1baa437c0 100644 --- a/packages/common/src/services/__tests__/extension.service.spec.ts +++ b/packages/common/src/services/__tests__/extension.service.spec.ts @@ -9,7 +9,6 @@ import { ContextMenuExtension, DraggableGroupingExtension, ExtensionUtility, - GridMenuExtension, GroupItemMetaProviderExtension, HeaderButtonExtension, HeaderMenuExtension, @@ -17,28 +16,70 @@ import { RowMoveManagerExtension, RowSelectionExtension, } from '../../extensions'; -import { ExtensionService, SharedService } from '..'; +import { BackendUtilityService, ExtensionService, FilterService, PubSubService, SharedService, SortService } from '..'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; import { AutoTooltipPlugin } from '../../plugins/index'; -import { ColumnPickerControl } from '../../controls/index'; +import { ColumnPickerControl, GridMenuControl } from '../../controls/index'; jest.mock('flatpickr', () => { }); declare const Slick: SlickNamespace; +const GRID_UID = 'slickgrid_12345'; + +const extensionUtilityStub = { + getPickerTitleOutputString: jest.fn(), + refreshBackendDataset: jest.fn(), + sortItems: jest.fn(), + translateItems: jest.fn(), + translateWhenEnabledAndServiceExist: jest.fn(), +} as unknown as ExtensionUtility; const gridStub = { autosizeColumns: jest.fn(), getColumnIndex: jest.fn(), getOptions: jest.fn(), getPluginByName: jest.fn(), - getUID: jest.fn(), + getUID: () => GRID_UID, getColumns: jest.fn(), setColumns: jest.fn(), onColumnsResized: jest.fn(), registerPlugin: jest.fn(), + onBeforeDestroy: new Slick.Event(), + onSetOptions: new Slick.Event(), onColumnsReordered: new Slick.Event(), onHeaderContextMenu: new Slick.Event(), } as unknown as SlickGrid; +const filterServiceStub = { + addRxJsResource: jest.fn(), + clearFilters: jest.fn(), + dispose: jest.fn(), + init: jest.fn(), + bindBackendOnFilter: jest.fn(), + bindLocalOnFilter: jest.fn(), + bindLocalOnSort: jest.fn(), + bindBackendOnSort: jest.fn(), + populateColumnFilterSearchTermPresets: jest.fn(), + refreshTreeDataFilters: jest.fn(), + getColumnFilters: jest.fn(), +} as unknown as FilterService; + +const pubSubServiceStub = { + publish: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + unsubscribeAll: jest.fn(), +} as PubSubService; + +const sortServiceStub = { + addRxJsResource: jest.fn(), + bindBackendOnSort: jest.fn(), + bindLocalOnSort: jest.fn(), + dispose: jest.fn(), + loadGridSorters: jest.fn(), + processTreeDataInitialSort: jest.fn(), + sortHierarchicalDataset: jest.fn(), +} as unknown as SortService; + const extensionStub = { create: jest.fn(), dispose: jest.fn(), @@ -84,26 +125,28 @@ const extensionRowMoveStub = { describe('ExtensionService', () => { let sharedService: SharedService; + let backendUtilityService: BackendUtilityService; let service: ExtensionService; - let extensionUtility: ExtensionUtility; let translateService: TranslateServiceStub; describe('with Translate Service', () => { beforeEach(() => { sharedService = new SharedService(); + backendUtilityService = new BackendUtilityService(); translateService = new TranslateServiceStub(); - extensionUtility = new ExtensionUtility(sharedService, translateService); translateService.use('fr'); service = new ExtensionService( - extensionUtility, + extensionUtilityStub, + filterServiceStub, + pubSubServiceStub, + sortServiceStub, // extensions extensionStub as unknown as CellExternalCopyManagerExtension, extensionCellMenuStub as unknown as CellMenuExtension, extensionCheckboxSelectorStub as unknown as CheckboxSelectorExtension, extensionContextMenuStub as unknown as ContextMenuExtension, extensionStub as unknown as DraggableGroupingExtension, - extensionGridMenuStub as unknown as GridMenuExtension, extensionGroupItemMetaStub as unknown as GroupItemMetaProviderExtension, extensionHeaderButtonStub as unknown as HeaderButtonExtension, extensionHeaderMenuStub as unknown as HeaderMenuExtension, @@ -155,7 +198,6 @@ describe('ExtensionService', () => { expect(spy).toHaveBeenCalled(); expect(output).toBeNull(); - }); it('should return extension addon when method is called with a valid and instantiated addon', () => { @@ -169,22 +211,31 @@ describe('ExtensionService', () => { expect(output).toEqual(instanceMock); }); - it('should register any addon and expect the instance returned from "getExtensionByName" equal the one returned from "getSlickgridAddonInstance"', () => { + it('should return Row Detail extension addon when method is called with a valid and instantiated addon', () => { const instanceMock = { onColumnsChanged: jest.fn() }; + const extensionMock = { name: ExtensionName.rowDetailView, instance: instanceMock as unknown, class: {} } as ExtensionModel; + const spy = jest.spyOn(service, 'getExtensionByName').mockReturnValue(extensionMock); + + const output = service.getSlickgridAddonInstance(ExtensionName.rowDetailView); + + expect(spy).toHaveBeenCalled(); + expect(output).toEqual(instanceMock); + }); + + it('should register any addon and expect the instance returned from "getExtensionByName" equal the one returned from "getSlickgridAddonInstance"', () => { const gridOptionsMock = { enableGridMenu: true } as GridOption; - const extSpy = jest.spyOn(extensionGridMenuStub, 'register').mockReturnValue(instanceMock); - const getAddonSpy = jest.spyOn(extensionGridMenuStub, 'getAddonInstance').mockReturnValue(instanceMock); const gridSpy = jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); service.bindDifferentExtensions(); + const gridMenuInstance = service.getSlickgridAddonInstance(ExtensionName.gridMenu); + const output = service.getExtensionByName(ExtensionName.gridMenu); const instance = service.getSlickgridAddonInstance(ExtensionName.gridMenu); expect(gridSpy).toHaveBeenCalled(); - expect(getAddonSpy).toHaveBeenCalled(); - expect(extSpy).toHaveBeenCalled(); + expect(gridMenuInstance).toBeTruthy(); expect(output!.instance).toEqual(instance); - expect(output).toEqual({ name: ExtensionName.gridMenu, instance: instanceMock as unknown, class: extensionGridMenuStub } as ExtensionModel); + expect(output).toEqual({ name: ExtensionName.gridMenu, instance: gridMenuInstance as unknown, class: {} } as ExtensionModel); }); }); @@ -277,20 +328,20 @@ describe('ExtensionService', () => { }); it('should register the GridMenu addon when "enableGridMenu" is set in the grid options', () => { - const gridOptionsMock = { enableGridMenu: true } as GridOption; - const extSpy = jest.spyOn(extensionGridMenuStub, 'register').mockReturnValue(instanceMock); - const getAddonSpy = jest.spyOn(extensionGridMenuStub, 'getAddonInstance').mockReturnValue(instanceMock); - const gridSpy = jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + const onRegisteredMock = jest.fn(); + const gridOptionsMock = { + enableGridMenu: true, + gridMenu: { + onExtensionRegistered: onRegisteredMock + } + } as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); service.bindDifferentExtensions(); const output = service.getExtensionByName(ExtensionName.gridMenu); - const instance = service.getSlickgridAddonInstance(ExtensionName.gridMenu); - expect(gridSpy).toHaveBeenCalled(); - expect(getAddonSpy).toHaveBeenCalled(); - expect(extSpy).toHaveBeenCalled(); - expect(output!.instance).toEqual(instance); - expect(output).toEqual({ name: ExtensionName.gridMenu, instance: instanceMock as unknown, class: extensionGridMenuStub } as ExtensionModel); + expect(onRegisteredMock).toHaveBeenCalledWith(expect.toBeObject()); + expect(output.instance instanceof GridMenuControl).toBeTrue(); }); it('should register the GroupItemMetaProvider addon when "enableGrouping" is set in the grid options', () => { @@ -519,7 +570,7 @@ describe('ExtensionService', () => { it('should call the refreshBackendDataset method on the GridMenu Extension when service with same method name is called', () => { const gridOptionsMock = { enableGridMenu: true } as GridOption; - const extSpy = jest.spyOn(extensionGridMenuStub, 'refreshBackendDataset'); + const extSpy = jest.spyOn(extensionUtilityStub, 'refreshBackendDataset'); jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); service.refreshBackendDataset(); @@ -529,6 +580,24 @@ describe('ExtensionService', () => { expect(extSpy).toHaveBeenNthCalledWith(2, gridOptionsMock); }); + it('should call all extensions translate methods when calling "translateAllExtensions"', () => { + const cellMenuSpy = jest.spyOn(service, 'translateCellMenu'); + const colHeaderSpy = jest.spyOn(service, 'translateColumnHeaders'); + const colPickerSpy = jest.spyOn(service, 'translateColumnPicker'); + const contextSpy = jest.spyOn(service, 'translateContextMenu'); + const gridMenuSpy = jest.spyOn(service, 'translateGridMenu'); + const headerMenuSpy = jest.spyOn(service, 'translateHeaderMenu'); + + service.translateAllExtensions(); + + expect(cellMenuSpy).toHaveBeenCalled(); + expect(colHeaderSpy).toHaveBeenCalled(); + expect(colPickerSpy).toHaveBeenCalled(); + expect(contextSpy).toHaveBeenCalled(); + expect(gridMenuSpy).toHaveBeenCalled(); + expect(headerMenuSpy).toHaveBeenCalled(); + }); + it('should call removeColumnByIndex and return original input when it is not an array provided', () => { const input = { foo: 'bar' }; // @ts-ignore:2345 @@ -568,9 +637,23 @@ describe('ExtensionService', () => { }); it('should call the translateGridMenu method on the GridMenu Extension when service with same method name is called', () => { - const extSpy = jest.spyOn(extensionGridMenuStub, 'translateGridMenu'); + const columnsMock = [ + { id: 'field1', field: 'field1', nameKey: 'HELLO' }, + { id: 'field2', field: 'field2', nameKey: 'WORLD' } + ] as Column[]; + const gridOptionsMock = { enableGridMenu: true } as GridOption; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(columnsMock); + + service.bindDifferentExtensions(); + service.renderColumnHeaders(columnsMock); + const gridMenuInstance = service.getSlickgridAddonInstance(ExtensionName.gridMenu); + + const extSpy = jest.spyOn(gridMenuInstance, 'translateGridMenu'); service.translateGridMenu(); + expect(extSpy).toHaveBeenCalled(); + expect(gridMenuInstance.columns).toEqual(columnsMock); }); it('should call the translateHeaderMenu method on the HeaderMenu Extension when service with same method name is called', () => { @@ -691,34 +774,23 @@ describe('ExtensionService', () => { expect(service.getExtensionByName(ExtensionName.columnPicker).instance.columns).toEqual(columnsMock); }); - it('should re-register the Grid Menu when enable and method is called with new column definition collection provided as argument', () => { - const instanceMock = { onColumnsChanged: jest.fn() }; - const extensionMock = { name: ExtensionName.gridMenu, addon: null, instance: null, class: null } as ExtensionModel; - const expectedExtension = { name: ExtensionName.gridMenu, instance: instanceMock as unknown, class: null } as ExtensionModel; - const gridOptionsMock = { enableGridMenu: true } as GridOption; + it('should replace the Grid Menu columns when plugin is enabled and method is called with new column definition collection provided as argument', () => { const columnsMock = [ { id: 'field1', field: 'field1', nameKey: 'HELLO' }, { id: 'field2', field: 'field2', nameKey: 'WORLD' } ] as Column[]; - + const instanceMock = { translateGridMenu: jest.fn() }; + const gridOptionsMock = { enableGridMenu: true } as GridOption; + jest.spyOn(extensionGridMenuStub, 'register').mockReturnValue(instanceMock); jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); - jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); - const spyGetExt = jest.spyOn(service, 'getExtensionByName').mockReturnValue(extensionMock); - const spyGmDispose = jest.spyOn(extensionGridMenuStub, 'dispose'); - const spyGmRegister = jest.spyOn(extensionGridMenuStub, 'register').mockReturnValue(instanceMock); - const spyAllCols = jest.spyOn(SharedService.prototype, 'allColumns', 'set'); + jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(columnsMock); const setColumnsSpy = jest.spyOn(gridStub, 'setColumns'); + service.bindDifferentExtensions(); service.renderColumnHeaders(columnsMock); - expect(expectedExtension).toEqual(expectedExtension); - expect(spyGetExt).toHaveBeenCalled(); - expect(expectedExtension).toEqual(expectedExtension); - expect(spyGetExt).toHaveBeenCalled(); - expect(spyGmDispose).toHaveBeenCalled(); - expect(spyGmRegister).toHaveBeenCalled(); - expect(spyAllCols).toHaveBeenCalledWith(columnsMock); expect(setColumnsSpy).toHaveBeenCalledWith(columnsMock); + expect(service.getExtensionByName(ExtensionName.gridMenu).instance.columns).toEqual(columnsMock); }); it('should re-register the Header Menu when enable and method is called with new column definition collection provided as argument', () => { @@ -754,19 +826,19 @@ describe('ExtensionService', () => { }); describe('without Translate Service', () => { - extensionUtility = new ExtensionUtility(sharedService, translateService); - beforeEach(() => { translateService = undefined as any; service = new ExtensionService( - extensionUtility, + extensionUtilityStub, + filterServiceStub, + pubSubServiceStub, + sortServiceStub, // extensions extensionStub as unknown as CellExternalCopyManagerExtension, extensionStub as unknown as CellMenuExtension, extensionStub as unknown as CheckboxSelectorExtension, extensionStub as unknown as ContextMenuExtension, extensionStub as unknown as DraggableGroupingExtension, - extensionGridMenuStub as unknown as GridMenuExtension, extensionGroupItemMetaStub as unknown as GroupItemMetaProviderExtension, extensionHeaderButtonStub as unknown as HeaderButtonExtension, extensionHeaderMenuStub as unknown as HeaderMenuExtension, diff --git a/packages/common/src/services/__tests__/utilities.spec.ts b/packages/common/src/services/__tests__/utilities.spec.ts index 6fc743847..060fb2b98 100644 --- a/packages/common/src/services/__tests__/utilities.spec.ts +++ b/packages/common/src/services/__tests__/utilities.spec.ts @@ -593,9 +593,14 @@ describe('Service/Utilies', () => { div.innerHTML = ``; document.body.appendChild(div); + it('should return undefined when element if not a valid html element', () => { + const output = getHtmlElementOffset(null); + expect(output).toEqual(undefined); + }); + it('should return top/left 0 when creating a new element in the document without positions', () => { const output = getHtmlElementOffset(div); - expect(output).toEqual({ top: 0, left: 0 }); + expect(output).toEqual({ top: 0, left: 0, bottom: 0, right: 0 }); }); it('should return same top/left positions as defined in the document/window', () => { diff --git a/packages/common/src/services/extension.service.ts b/packages/common/src/services/extension.service.ts index 4788d2bc9..918d49b9f 100644 --- a/packages/common/src/services/extension.service.ts +++ b/packages/common/src/services/extension.service.ts @@ -12,7 +12,6 @@ import { ContextMenuExtension, DraggableGroupingExtension, ExtensionUtility, - GridMenuExtension, GroupItemMetaProviderExtension, HeaderButtonExtension, HeaderMenuExtension, @@ -23,7 +22,10 @@ import { import { SharedService } from './shared.service'; import { TranslaterService } from './translater.service'; import { AutoTooltipPlugin } from '../plugins/index'; -import { ColumnPickerControl } from '../controls/index'; +import { ColumnPickerControl, GridMenuControl } from '../controls/index'; +import { FilterService } from './filter.service'; +import { PubSubService } from './pubSub.service'; +import { SortService } from './sort.service'; interface ExtensionWithColumnIndexPosition { name: ExtensionName; @@ -33,6 +35,7 @@ interface ExtensionWithColumnIndexPosition { export class ExtensionService { protected _columnPickerControl?: ColumnPickerControl; + protected _gridMenuControl?: GridMenuControl; protected _extensionCreatedList: ExtensionList = {} as ExtensionList; protected _extensionList: ExtensionList = {} as ExtensionList; @@ -46,12 +49,15 @@ export class ExtensionService { constructor( protected readonly extensionUtility: ExtensionUtility, + protected readonly filterService: FilterService, + protected readonly pubSubService: PubSubService, + protected readonly sortService: SortService, + protected readonly cellExternalCopyExtension: CellExternalCopyManagerExtension, protected readonly cellMenuExtension: CellMenuExtension, protected readonly checkboxSelectorExtension: CheckboxSelectorExtension, protected readonly contextMenuExtension: ContextMenuExtension, protected readonly draggableGroupingExtension: DraggableGroupingExtension, - protected readonly gridMenuExtension: GridMenuExtension, protected readonly groupItemMetaExtension: GroupItemMetaProviderExtension, protected readonly headerButtonExtension: HeaderButtonExtension, protected readonly headerMenuExtension: HeaderMenuExtension, @@ -73,6 +79,9 @@ export class ExtensionService { if (extension?.class?.dispose) { extension.class.dispose(); } + if (extension?.instance?.dispose) { + extension.instance.dispose(); + } } } for (const key of Object.keys(this._extensionList)) { @@ -109,10 +118,7 @@ export class ExtensionService { getSlickgridAddonInstance(name: ExtensionName): any { const extension = this.getExtensionByName(name); if (extension && extension.class && (extension.instance)) { - if (extension.class && extension.class.getAddonInstance) { - return extension.class.getAddonInstance(); - } - return extension.instance; + return extension.class?.getAddonInstance?.() ?? extension.instance; } return null; } @@ -180,7 +186,7 @@ export class ExtensionService { // Column Picker Control if (this.gridOptions.enableColumnPicker) { - this._columnPickerControl = new ColumnPickerControl(this.extensionUtility, this.sharedService); + this._columnPickerControl = new ColumnPickerControl(this.extensionUtility, this.pubSubService, this.sharedService); if (this._columnPickerControl) { if (this.gridOptions.columnPicker?.onExtensionRegistered) { this.gridOptions.columnPicker.onExtensionRegistered(this._columnPickerControl); @@ -206,10 +212,13 @@ export class ExtensionService { } // Grid Menu Control - if (this.gridOptions.enableGridMenu && this.gridMenuExtension && this.gridMenuExtension.register) { - const instance = this.gridMenuExtension.register(); - if (instance) { - this._extensionList[ExtensionName.gridMenu] = { name: ExtensionName.gridMenu, class: this.gridMenuExtension, instance }; + if (this.gridOptions.enableGridMenu) { + this._gridMenuControl = new GridMenuControl(this.extensionUtility, this.filterService, this.pubSubService, this.sharedService, this.sortService); + if (this._gridMenuControl) { + if (this.gridOptions.gridMenu?.onExtensionRegistered) { + this.gridOptions.gridMenu.onExtensionRegistered(this._gridMenuControl); + } + this._extensionList[ExtensionName.gridMenu] = { name: ExtensionName.gridMenu, class: {}, instance: this._gridMenuControl }; } } @@ -318,7 +327,7 @@ export class ExtensionService { /** Refresh the dataset through the Backend Service */ refreshBackendDataset(gridOptions?: GridOption) { - this.gridMenuExtension.refreshBackendDataset(gridOptions); + this.extensionUtility.refreshBackendDataset(gridOptions); } /** @@ -333,6 +342,16 @@ export class ExtensionService { return columns; } + /** Translate all possible Extensions at once */ + translateAllExtensions() { + this.translateCellMenu(); + this.translateColumnHeaders(); + this.translateColumnPicker(); + this.translateContextMenu(); + this.translateGridMenu(); + this.translateHeaderMenu(); + } + /** Translate the Cell Menu titles, we need to loop through all column definition to re-translate them */ translateCellMenu() { if (this.cellMenuExtension && this.cellMenuExtension.translateCellMenu) { @@ -358,9 +377,7 @@ export class ExtensionService { * Translate the Header Menu titles, we need to loop through all column definition to re-translate them */ translateGridMenu() { - if (this.gridMenuExtension && this.gridMenuExtension.translateGridMenu) { - this.gridMenuExtension.translateGridMenu(); - } + this._gridMenuControl?.translateGridMenu?.(); } /** @@ -397,7 +414,7 @@ export class ExtensionService { // re-render the column headers this.renderColumnHeaders(columnDefinitions, Array.isArray(newColumnDefinitions)); - this.gridMenuExtension.translateGridMenu(); + this._gridMenuControl?.translateGridMenu?.(); } /** @@ -421,9 +438,9 @@ export class ExtensionService { this._columnPickerControl.columns = this.sharedService.allColumns; } - // recreate the Grid Menu when enabled - if (this.gridOptions.enableGridMenu) { - this.recreateExternalAddon(this.gridMenuExtension, ExtensionName.gridMenu); + // replace the Grid Menu columns array list + if (this._gridMenuControl) { + this._gridMenuControl.columns = this.sharedService.allColumns ?? []; } // recreate the Header Menu when enabled diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts index 2af32e1b5..dee34b5fa 100644 --- a/packages/common/src/services/utilities.ts +++ b/packages/common/src/services/utilities.ts @@ -1018,16 +1018,23 @@ export function findOrDefault(array: T[], logic: (item: T) => boolean, } /** Get HTML Element position offset (without jQuery) */ -export function getHtmlElementOffset(element: HTMLElement): { top: number; left: number; } { +export function getHtmlElementOffset(element: HTMLElement): { top: number; bottom: number; left: number; right: number; } | undefined { + if (!element) { + return undefined; + } const rect = element?.getBoundingClientRect?.(); let top = 0; let left = 0; + let bottom = 0; + let right = 0; if (rect && rect.top !== undefined && rect.left !== undefined) { top = rect.top + window.pageYOffset; left = rect.left + window.pageXOffset; + right = rect.right; + bottom = rect.bottom; } - return { top, left }; + return { top, left, bottom, right }; } /** diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts index 3ee30b438..ff0c53f95 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts @@ -68,6 +68,7 @@ const extensionServiceStub = { createExtensionsBeforeGridCreation: jest.fn(), dispose: jest.fn(), renderColumnHeaders: jest.fn(), + translateAllExtensions: jest.fn(), translateCellMenu: jest.fn(), translateColumnHeaders: jest.fn(), translateColumnPicker: jest.fn(), @@ -1377,12 +1378,7 @@ describe('Slick-Vanilla-Grid-Bundle 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 transExtensionSpy = jest.spyOn(extensionServiceStub, 'translateAllExtensions'); const transGroupingColSpanSpy = jest.spyOn(groupingAndColspanServiceStub, 'translateGroupingAndColSpan'); const setHeaderRowSpy = jest.spyOn(mockGrid, 'setHeaderRowVisibility'); @@ -1395,12 +1391,7 @@ describe('Slick-Vanilla-Grid-Bundle 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(transExtensionSpy).toHaveBeenCalled(); expect(transCustomFooterSpy).toHaveBeenCalled(); done(); }); @@ -1408,12 +1399,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () 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 transExtensionSpy = jest.spyOn(extensionServiceStub, 'translateAllExtensions'); const transGroupingColSpanSpy = jest.spyOn(groupingAndColspanServiceStub, 'translateGroupingAndColSpan'); component.gridOptions = { enableTranslate: true, createPreHeaderPanel: true, enableDraggableGrouping: false } as unknown as GridOption; @@ -1423,12 +1409,7 @@ describe('Slick-Vanilla-Grid-Bundle 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(transExtensionSpy).toHaveBeenCalled(); done(); }); }); diff --git a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts index a264d996d..db38c48cc 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -38,7 +38,6 @@ import { ContextMenuExtension, DraggableGroupingExtension, ExtensionUtility, - GridMenuExtension, GroupItemMetaProviderExtension, HeaderMenuExtension, HeaderButtonExtension, @@ -349,7 +348,7 @@ export class SlickVanillaGridBundle { this.gridEventService = services?.gridEventService ?? new GridEventService(); this.sharedService = services?.sharedService ?? new SharedService(); this.collectionService = services?.collectionService ?? new CollectionService(this.translaterService); - this.extensionUtility = services?.extensionUtility ?? new ExtensionUtility(this.sharedService, this.translaterService); + this.extensionUtility = services?.extensionUtility ?? new ExtensionUtility(this.sharedService, this.backendUtilityService, this.translaterService); this.filterFactory = new FilterFactory(slickgridConfig, this.translaterService, this.collectionService); this.filterService = services?.filterService ?? new FilterService(this.filterFactory, this._eventPubSubService, this.sharedService, this.backendUtilityService); this.resizerService = services?.resizerService ?? new ResizerService(this._eventPubSubService); @@ -363,7 +362,6 @@ export class SlickVanillaGridBundle { const contextMenuExtension = new ContextMenuExtension(this.extensionUtility, this.sharedService, this.treeDataService, this.translaterService); const checkboxExtension = new CheckboxSelectorExtension(this.sharedService); const draggableGroupingExtension = new DraggableGroupingExtension(this.extensionUtility, 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); @@ -373,12 +371,14 @@ export class SlickVanillaGridBundle { this.extensionService = services?.extensionService ?? new ExtensionService( this.extensionUtility, + this.filterService, + this._eventPubSubService, + this.sortService, cellExternalCopyManagerExtension, cellMenuExtension, checkboxExtension, contextMenuExtension, draggableGroupingExtension, - gridMenuExtension, groupItemMetaProviderExtension, headerButtonExtension, headerMenuExtension, @@ -755,12 +755,7 @@ export class SlickVanillaGridBundle { this.subscriptions.push( this._eventPubSubService.subscribe('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.translateCustomFooterTexts(); this.translateColumnHeaderTitleKeys(); this.translateColumnGroupKeys(); diff --git a/packages/vanilla-bundle/src/index.spec.ts b/packages/vanilla-bundle/src/index.spec.ts index 6d0d80311..5f1a1245b 100644 --- a/packages/vanilla-bundle/src/index.spec.ts +++ b/packages/vanilla-bundle/src/index.spec.ts @@ -8,6 +8,7 @@ describe('Testing library entry point', () => { it('should have all exported object defined', () => { expect(typeof entry.Slicker).toBe('object'); expect(typeof entry.BindingService).toBe('function'); + expect(typeof entry.EventPubSubService).toBe('function'); expect(typeof entry.Slicker.GridBundle).toBe('function'); expect(typeof entry.Slicker.Aggregators).toBe('object'); expect(typeof entry.Slicker.BindingService).toBe('function'); @@ -20,6 +21,7 @@ describe('Testing library entry point', () => { expect(typeof entry.Slicker.Utilities).toBe('object'); expect(typeof entry.SlickCompositeEditorComponent).toBe('function'); expect(typeof entry.SlickEmptyWarningComponent).toBe('function'); + expect(typeof entry.SlickPaginationComponent).toBe('function'); expect(typeof entry.SlickVanillaGridBundle).toBe('function'); expect(typeof entry.Aggregators).toBe('object'); expect(typeof entry.Editors).toBe('object'); diff --git a/test/cypress/integration/example01.spec.js b/test/cypress/integration/example01.spec.js index 5fa08e445..c1ea4691d 100644 --- a/test/cypress/integration/example01.spec.js +++ b/test/cypress/integration/example01.spec.js @@ -7,7 +7,10 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { beforeEach(() => { // add a serve mode to avoid adding the GitHub Stars link since that can slowdown Cypress considerably // because it keeps waiting for it to load, we also preserve the cookie for all other tests - cy.setCookie('serve-mode', 'cypress') + cy.setCookie('serve-mode', 'cypress'); + + // create a console.log spy for later use + cy.window().then(win => cy.spy(win.console, 'log')); }) it('should display Example title', () => { @@ -125,11 +128,8 @@ describe('Example 01 - 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') - .trigger('click') .click(); - }); - it('should have no sorting in 2nd grid (back to default sorted by id)', () => { let gridUid = ''; cy.get('.grid2 .slickgrid-container') @@ -139,26 +139,36 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { expect(gridUid).to.not.be.null; }) .then(() => { - cy.get(`.slick-gridmenu.${gridUid}`) + cy.get(`.slick-gridmenu.${gridUid}.dropright`) .find('.slick-gridmenu-item:nth(1)') .find('span') .contains('Clear all Sorting') .click(); + }); - cy.get('.grid2') - .find('.slick-sort-indicator-asc') - .should('have.length', 0); + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(4); + expect(win.console.log).to.be.calledWith('gridMenu:onBeforeMenuShow'); + expect(win.console.log).to.be.calledWith('gridMenu:onAfterMenuShow'); + expect(win.console.log).to.be.calledWith('gridMenu:onCommand', 'clear-sorting'); + expect(win.console.log).to.be.calledWith('gridMenu:onMenuClose - visible columns count', 6); + }); + }); - cy.get('.grid2') - .find('.slick-sort-indicator-desc') - .should('have.length', 0); + it('should have no sorting in 2nd grid (back to default sorted by id)', () => { + cy.get('.grid2') + .find('.slick-sort-indicator-asc') + .should('have.length', 0); - cy.get(`.grid2 [style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0)`).should('contain', 'Task 23'); - cy.get(`.grid2 [style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0)`).should('contain', 'Task 24'); - cy.get(`.grid2 [style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0)`).should('contain', 'Task 25'); - cy.get(`.grid2 [style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(0)`).should('contain', 'Task 26'); - cy.get(`.grid2 [style="top:${GRID_ROW_HEIGHT * 4}px"] > .slick-cell:nth(0)`).should('contain', 'Task 27'); - }); + cy.get('.grid2') + .find('.slick-sort-indicator-desc') + .should('have.length', 0); + + cy.get(`.grid2 [style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0)`).should('contain', 'Task 23'); + cy.get(`.grid2 [style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0)`).should('contain', 'Task 24'); + cy.get(`.grid2 [style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0)`).should('contain', 'Task 25'); + cy.get(`.grid2 [style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(0)`).should('contain', 'Task 26'); + cy.get(`.grid2 [style="top:${GRID_ROW_HEIGHT * 4}px"] > .slick-cell:nth(0)`).should('contain', 'Task 27'); }); it('should retain sorting in 1st grid', () => { @@ -186,9 +196,7 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { }); it('should clear filters of grid2 using the Grid Menu "Clear all Filters" command', () => { - cy.get('.grid2') - .find('button.slick-gridmenu-button') - .trigger('click') + cy.get('[data-test="external-gridmenu2-btn"]') .click(); let gridUid = ''; @@ -200,12 +208,20 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { expect(gridUid).to.not.be.null; }) .then(() => { - cy.get(`.slick-gridmenu.${gridUid}`) + cy.get(`.slick-gridmenu.${gridUid}.dropleft`) .find('.slick-gridmenu-item:nth(0)') .find('span') .contains('Clear all Filters') .click(); }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(4); + expect(win.console.log).to.be.calledWith('gridMenu:onBeforeMenuShow'); + expect(win.console.log).to.be.calledWith('gridMenu:onAfterMenuShow'); + expect(win.console.log).to.be.calledWith('gridMenu:onCommand', 'clear-filter'); + expect(win.console.log).to.be.calledWith('gridMenu:onMenuClose - visible columns count', 6); + }); }); it('should change Page Number 52 and expect the Pagination to have correct values', () => { @@ -247,7 +263,7 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { 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]); } }); @@ -295,7 +311,7 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { 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]); } }); @@ -335,9 +351,9 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { const $input = $child.children('input'); const $label = $child.children('label'); if ($label.text() === 'Title') { - expect($input.attr('checked')).to.eq(undefined); + expect($input.prop('checked')).to.eq(false); } else { - expect($input.attr('checked')).to.eq('checked'); + expect($input.prop('checked')).to.eq(true); } expect($label.text()).to.eq(fullTitles[index]); } @@ -389,6 +405,11 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { .should('contain', '% Complete') .click(); + cy.get('.grid2') + .get('.slick-columnpicker:visible') + .find('span.close') + .click(); + cy.get('.grid2') .find('.slick-header-columns') .children() @@ -398,11 +419,10 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { } }); - cy.get('.grid2') - .get('.slick-columnpicker:visible') - .find('span.close') - .trigger('click') - .click(); + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('columnPicker:onColumnsChanged - visible columns count', 6); + }); }); it('should open the Grid Menu on 2nd Grid and expect all Columns to be checked', () => { @@ -425,7 +445,7 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { 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]); } }); @@ -462,9 +482,9 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { const $input = $child.children('input'); const $label = $child.children('label'); if ($label.text() === 'Title' || $label.text() === 'Start') { - expect($input.attr('checked')).to.eq(undefined); + expect($input.prop('checked')).to.eq(false); } else { - expect($input.attr('checked')).to.eq('checked'); + expect($input.prop('checked')).to.eq(true); } expect($label.text()).to.eq(fullTitles[index]); } diff --git a/test/cypress/integration/example02.spec.js b/test/cypress/integration/example02.spec.js index 577400272..774ad7294 100644 --- a/test/cypress/integration/example02.spec.js +++ b/test/cypress/integration/example02.spec.js @@ -34,9 +34,8 @@ describe('Example 02 - Grouping & Aggregators', { retries: 1 }, () => { .find('.slick-custom-footer') .find('.right-footer') .should($span => { - const dateTime = moment().format('YYYY-MM-DD, hh:mm a'); const text = removeExtraSpaces($span.text()); // remove all white spaces - expect(text).to.eq(`Last Update ${dateTime} | 500 of 500 items`); + expect(text).to.eq(`Last Update ${moment().format('YYYY-MM-DD, hh:mm a')} | 500 of 500 items`); }); }); @@ -92,7 +91,7 @@ describe('Example 02 - Grouping & Aggregators', { retries: 1 }, () => { cy.get('.grid2') .find('button.slick-gridmenu-button') .trigger('click') - .click(); + .click({ force: true }); }); describe('Grouping Tests', () => { diff --git a/test/cypress/integration/example05.spec.js b/test/cypress/integration/example05.spec.js index 57c36942d..6c2d1e509 100644 --- a/test/cypress/integration/example05.spec.js +++ b/test/cypress/integration/example05.spec.js @@ -132,7 +132,7 @@ describe('Example 05 - Tree Data (from a flat dataset with parentId references)' cy.get('.grid5') .find('button.slick-gridmenu-button') .trigger('click') - .click(); + .click({ force: true }); let gridUid = ''; diff --git a/test/cypress/integration/example06.spec.js b/test/cypress/integration/example06.spec.js index 2f7788c53..3c650938c 100644 --- a/test/cypress/integration/example06.spec.js +++ b/test/cypress/integration/example06.spec.js @@ -136,14 +136,14 @@ describe('Example 06 - Tree Data (from a Hierarchical Dataset)', { retries: 1 }, cy.get('.grid6') .find('button.slick-gridmenu-button') .trigger('click') - .click(); + .click({ force: true }); cy.get(`.slick-gridmenu:visible`) .find('.slick-gridmenu-item') .first() .find('span') .contains('Clear all Filters') - .click(); + .click({ force: true }); defaultSortAscList.forEach((_colName, rowIdx) => { if (rowIdx > defaultSortAscList.length - 1) { diff --git a/test/cypress/integration/example07.spec.js b/test/cypress/integration/example07.spec.js index c28a0a46b..8d6172d37 100644 --- a/test/cypress/integration/example07.spec.js +++ b/test/cypress/integration/example07.spec.js @@ -206,6 +206,29 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(10)`).should('contain', 'Task 0'); }); + it('should open Grid Menu and expect new columns to be added to the column picker section', () => { + const updatedTitles = ['', '', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed', 'Prerequisites', 'Title', 'Title']; + + cy.get('.grid7') + .find('button.slick-gridmenu-button') + .click({ force: true }); + + cy.get('.grid7 .slickgrid-container') + .then(() => { + cy.get(`.slick-gridmenu`) + .find('.slick-gridmenu-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.children('input'); + const $label = $child.children('label'); + expect($input.prop('checked')).to.eq(true); + expect($label.text()).to.eq(updatedTitles[index]); + } + }); + }); + }); + it('should be able to filter and search "Task 2222" in the new column and expect only 1 row showing in the grid', () => { cy.get('input.search-filter.filter-title1') .type('Task 2222', { force: true }) @@ -672,6 +695,58 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries .contains('2 of 501 items'); }); + it('should reorder "Start" column to be after the "Completed" column', () => { + const expectedTitles = ['', '', 'Title', '% Complete', 'Finish', 'Duration', 'Completed', 'Start', 'Prerequisites', 'Title']; + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(4)') + .should('contain', 'Start') + .trigger('mousedown', 'bottom', { which: 1 }); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(7)') + .should('contain', 'Completed') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { force: true }); + + cy.get('.grid7') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + }); + + it('should hide "Duration" column from column picker', () => { + const originalColumns = ['', '', 'Title', '% Complete', 'Finish', 'Duration', 'Completed', 'Start', 'Prerequisites', 'Title']; + + cy.get('.grid7') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-columnpicker') + .find('.slick-columnpicker-list') + .children() + .each(($child, index) => { + if (index < originalColumns.length) { + expect($child.text()).to.eq(originalColumns[index]); + } + }); + + cy.get('.slick-columnpicker') + .find('.slick-columnpicker-list') + .children('li:nth-child(6)') + .children('label') + .should('contain', 'Duration') + .click(); + + cy.get('.slick-columnpicker:visible') + .find('span.close') + .trigger('click') + .click(); + }); + it('should switch language', () => { cy.get('[data-test="language-button"]') .click(); @@ -693,4 +768,31 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries cy.get('.right-footer.metrics') .contains('2 de 501 éléments'); }); + + it('should open Grid Menu and expect new columns to be added to the column picker section, also "Duration" to be unchecked while "Finish" to be at new position', () => { + const updatedTitles = ['', '', 'Titre', 'Durée', '% Achevée', 'Fin', 'Terminé', 'Début', 'Prerequisites', 'Titre']; + + cy.get('.grid7') + .find('button.slick-gridmenu-button') + .click({ force: true }); + + cy.get('.grid7 .slickgrid-container') + .then(() => { + cy.get(`.slick-gridmenu`) + .find('.slick-gridmenu-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.children('input'); + const $label = $child.children('label'); + if ($label.text() === 'Durée') { + expect($input.prop('checked')).to.eq(false); + } else { + expect($input.prop('checked')).to.eq(true); + } + expect($label.text()).to.eq(updatedTitles[index]); + } + }); + }); + }); }); diff --git a/test/cypress/integration/example09.spec.js b/test/cypress/integration/example09.spec.js index 8f9069a8f..8a2abc156 100644 --- a/test/cypress/integration/example09.spec.js +++ b/test/cypress/integration/example09.spec.js @@ -198,8 +198,7 @@ describe('Example 09 - OData Grid', { retries: 1 }, () => { it('should Clear all Filters and expect to go back to first page', () => { cy.get('.grid9') .find('button.slick-gridmenu-button') - .trigger('click') - .click(); + .click({ force: true }); cy.get(`.slick-gridmenu:visible`) .find('.slick-gridmenu-item') diff --git a/test/cypress/integration/example15.spec.js b/test/cypress/integration/example15.spec.js index d168e9fc4..e2b2e8847 100644 --- a/test/cypress/integration/example15.spec.js +++ b/test/cypress/integration/example15.spec.js @@ -5,9 +5,7 @@ describe('Example 15 - OData Grid using RxJS', { retries: 1 }, () => { beforeEach(() => { // create a console.log spy for later use - cy.window().then((win) => { - cy.spy(win.console, 'log'); - }); + cy.window().then(win => cy.spy(win.console, 'log')); }); it('should display Example title', () => { @@ -201,7 +199,7 @@ describe('Example 15 - OData Grid using RxJS', { retries: 1 }, () => { cy.get('.grid15') .find('button.slick-gridmenu-button') .trigger('click') - .click(); + .click({ force: true }); cy.get(`.slick-gridmenu:visible`) .find('.slick-gridmenu-item') @@ -307,7 +305,7 @@ describe('Example 15 - OData Grid using RxJS', { retries: 1 }, () => { }); it('should click on Set Dynamic Filter and expect query and filters to be changed', () => { - cy.get('[data-test=set-dynamic-filter]') + cy.get('[data-test=set-dynamic-filter-btn]') .click(); cy.get('.search-filter.filter-name select') @@ -437,7 +435,7 @@ describe('Example 15 - OData Grid using RxJS', { retries: 1 }, () => { }); it('should click on Set Dynamic Filter and expect query and filters to be changed', () => { - cy.get('[data-test=set-dynamic-filter]') + cy.get('[data-test=set-dynamic-filter-btn]') .click(); cy.get('.search-filter.filter-name select') @@ -593,14 +591,14 @@ describe('Example 15 - OData Grid using RxJS', { retries: 1 }, () => { describe('Set Dynamic Sorting', () => { it('should click on "Set Filters Dynamically" then on "Set Sorting Dynamically"', () => { - cy.get('[data-test=set-dynamic-filter]') + cy.get('[data-test=set-dynamic-filter-btn]') .click(); // wait for the query to finish cy.get('[data-test=status]').should('contain', 'loading'); cy.get('[data-test=status]').should('contain', 'finished'); - cy.get('[data-test=set-dynamic-sorting]') + cy.get('[data-test=set-dynamic-sorting-btn]') .click(); cy.get('[data-test=status]').should('contain', 'loading'); @@ -676,9 +674,9 @@ describe('Example 15 - OData Grid using RxJS', { retries: 1 }, () => { }); it('should click on "Add Other Gender via RxJS" button', () => { - cy.get('[data-test="add-gender-button"]').should('not.be.disabled'); - cy.get('[data-test="add-gender-button"]').click(); - cy.get('[data-test="add-gender-button"]').should('be.disabled'); + cy.get('[data-test="add-gender-btn"]').should('not.be.disabled'); + cy.get('[data-test="add-gender-btn"]').click(); + cy.get('[data-test="add-gender-btn"]').should('be.disabled'); }); it('should open the "Gender" editor on the first row and expect to find 1 more option the editor list (male, female, other)', () => {