diff --git a/.vscode/launch.json b/.vscode/launch.json index d71d23a07..0c757417b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,7 +29,7 @@ "--runInBand", "${fileBasename}", "--config", - "${workspaceFolder}/test/jest.config.js" + "${workspaceFolder}/test/jest.config.ts" ], "console": "internalConsole", "internalConsoleOptions": "neverOpen", diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts index 2bd37ba8e..1aa3b56a2 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts @@ -470,7 +470,7 @@ export default class Example11 { } remoteCallbackFn(args: { item: any, selectedIds: string[], updateType: 'selection' | 'mass' }) { - const fields: Array<{ fieldName: string; value: any;}> = []; + const fields: Array<{ fieldName: string; value: any; }> = []; for (const key in args.item) { if (args.item.hasOwnProperty(key)) { fields.push({ fieldName: key, value: args.item[key] }); @@ -675,6 +675,7 @@ export default class Example11 { this.sgb.gridStateService.resetToOriginalColumns(); this.sgb.filterService.clearFilters(); this.sgb.sortService.clearSorting(); + this.sgb.gridService.clearPinning(); } async updateView(event: DOMEvent) { diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts index dfe8b68ee..614916cf7 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts @@ -133,6 +133,7 @@ export class Example12 { this._bindingEventService.bind(this.gridContainerElm, 'oncompositeeditorchange', this.handleOnCompositeEditorChange.bind(this)); this._bindingEventService.bind(this.gridContainerElm, 'onpaginationchanged', this.handleReRenderUnsavedStyling.bind(this)); this._bindingEventService.bind(this.gridContainerElm, 'onfilterchanged', this.handleReRenderUnsavedStyling.bind(this)); + this._bindingEventService.bind(this.gridContainerElm, 'onselectedrowidschanged', this.handleOnSelectedRowIdsChanged.bind(this)); } dispose() { @@ -433,8 +434,8 @@ export class Example12 { headerRowHeight: 35, enableCheckboxSelector: true, enableRowSelection: true, - multiSelect: false, checkboxSelector: { + applySelectOnAllPages: true, hideInFilterHeaderRow: false, hideInColumnTitleRow: true, }, @@ -623,6 +624,12 @@ export class Example12 { } } + handleOnSelectedRowIdsChanged(event) { + const args = event?.detail?.args; + // const sortedSelectedIds = args.filteredIds.sort((a, b) => a - b); + console.log('sortedSelectedIds', args.filteredIds.length, args.selectedRowIds.length); + } + toggleGridEditReadonly() { // first need undo all edits this.undoAllEdits(); diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example14.html b/examples/webpack-demo-vanilla-bundle/src/examples/example14.html index f1d371dea..3c28a1bea 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example14.html +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example14.html @@ -49,7 +49,7 @@

Container Width (1000px)

@@ -65,6 +65,12 @@

Container Width (1000px)

Save All

+

+ +

diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts index 181f5012f..a9c885dc0 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts @@ -19,6 +19,7 @@ import { // utilities formatNumber, Utilities, + GridStateChange, } from '@slickgrid-universal/common'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { Slicker, SlickerGridInstance, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; @@ -26,7 +27,7 @@ import { Slicker, SlickerGridInstance, SlickVanillaGridBundle } from '@slickgrid import { ExampleGridOptions } from './example-grid-options'; import './example14.scss'; -const NB_ITEMS = 5000; +const NB_ITEMS = 400; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; @@ -122,6 +123,8 @@ export class Example14 { this._bindingEventService.bind(this.gridContainerElm, 'onpaginationchanged', this.handlePaginationChanged.bind(this)); this._bindingEventService.bind(this.gridContainerElm, 'onbeforeresizebycontent', this.showSpinner.bind(this)); this._bindingEventService.bind(this.gridContainerElm, 'onafterresizebycontent', this.hideSpinner.bind(this)); + this._bindingEventService.bind(this.gridContainerElm, 'onselectedrowidschanged', this.handleOnSelectedRowIdsChanged.bind(this)); + this._bindingEventService.bind(this.gridContainerElm, 'ongridstatechanged', this.handleOnGridStateChanged.bind(this)); } dispose() { @@ -361,8 +364,14 @@ export class Example14 { autoResize: { container: '.grid-container', resizeDetection: 'container', + minHeight: 250 }, enableAutoResize: true, + enablePagination: true, + pagination: { + pageSize: 10, + pageSizes: [10, 200, 500, 5000] + }, // resizing by cell content is opt-in // we first need to disable the 2 default flags to autoFit/autosize @@ -383,6 +392,7 @@ export class Example14 { enableRowSelection: true, enableCheckboxSelector: true, checkboxSelector: { + applySelectOnAllPages: true, // already defaults to true hideInFilterHeaderRow: false, hideInColumnTitleRow: true, }, @@ -474,6 +484,12 @@ export class Example14 { return tmpArray; } + handleOnGridStateChanged(event) { + // console.log('handleOnGridStateChanged', event?.detail ?? '') + const gridStateChanges: GridStateChange = event?.detail; + console.log('Grid State changed::', gridStateChanges); + } + handleValidationError(event) { console.log('handleValidationError', event.detail); const args = event.detail && event.detail.args; @@ -545,6 +561,11 @@ export class Example14 { this.classNewResizeButton = 'button is-small is-selected is-primary'; } + handleOnSelectedRowIdsChanged(event) { + const args = event?.detail?.args ?? {}; + console.log('Selected Ids:', args.selectedRowIds); + } + toggleGridEditReadonly() { // first need undo all edits this.undoAllEdits(); @@ -616,6 +637,19 @@ export class Example14 { this.editedItems = {}; } + // change row selection dynamically and apply it to the DataView and the Grid UI + setSelectedRowIds() { + // change row selection even across multiple pages via DataView + this.sgb.dataView?.setSelectedIds([3, 4, 11]); + + // you can also provide optional options (all defaults to true) + // this.sgb.dataView?.setSelectedIds([4, 5, 8, 10], { + // isRowBeingAdded: true, + // shouldTriggerEvent: true, + // applyGridRowSelection: true + // }); + } + undoLastEdit(showLastEditor = false) { const lastEdit = this.editQueue.pop(); const lastEditCommand = lastEdit?.editCommand; diff --git a/package.json b/package.json index 74993e50f..489a104fe 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "rimraf": "^3.0.2", "rxjs": "^7.5.7", "serve": "^14.2.0", - "slickgrid": "^3.0.2", + "slickgrid": "^3.0.3", "sortablejs": "^1.15.0", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", diff --git a/packages/common/package.json b/packages/common/package.json index 7aa5339ef..eebbdb7a3 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -82,7 +82,7 @@ "jquery": "^3.6.3", "moment-mini": "^2.29.4", "multiple-select-modified": "^1.3.17", - "slickgrid": "^3.0.2", + "slickgrid": "^3.0.3", "sortablejs": "^1.15.0", "un-flatten-tree": "^2.0.12" }, diff --git a/packages/common/src/extensions/__tests__/slickCheckboxSelectColumn.spec.ts b/packages/common/src/extensions/__tests__/slickCheckboxSelectColumn.spec.ts index e38daaac3..15a30d806 100644 --- a/packages/common/src/extensions/__tests__/slickCheckboxSelectColumn.spec.ts +++ b/packages/common/src/extensions/__tests__/slickCheckboxSelectColumn.spec.ts @@ -26,6 +26,18 @@ const addJQueryEventPropagation = function (event, commandKey = '', keyName = '' return event; } +const dataViewStub = { + collapseAllGroups: jest.fn(), + getAllSelectedFilteredIds: jest.fn(), + getFilteredItems: jest.fn(), + getItemByIdx: jest.fn(), + getItemCount: jest.fn(), + getIdPropertyName: () => 'id', + onPagingInfoChanged: new Slick.Event(), + onSelectedRowIdsChanged: new Slick.Event(), + setSelectedIds: jest.fn(), +} + const getEditorLockMock = { commitCurrentEdit: jest.fn(), isActive: jest.fn(), @@ -34,6 +46,7 @@ const getEditorLockMock = { const gridStub = { getEditorLock: () => getEditorLockMock, getColumns: jest.fn(), + getData: () => dataViewStub, getDataItem: jest.fn(), getDataLength: jest.fn(), getOptions: jest.fn(), @@ -87,11 +100,6 @@ describe('SlickCheckboxSelectColumn Plugin', () => { Slick.RowMoveManager = mockAddon; let mockColumns: Column[]; - // let mockColumns: Column[] = [ - // { id: 'firstName', field: 'firstName', name: 'First Name', }, - // { id: 'lastName', field: 'lastName', name: 'Last Name', }, - // { id: 'age', field: 'age', name: 'Age', }, - // ]; let plugin: SlickCheckboxSelectColumn; beforeEach(() => { @@ -103,7 +111,6 @@ describe('SlickCheckboxSelectColumn Plugin', () => { plugin = new SlickCheckboxSelectColumn(pubSubServiceStub); jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(false); jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); - // jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue([]); }); afterEach(() => { @@ -113,6 +120,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => { it('should create the plugin with default options', () => { const expectedOptions = { + applySelectOnAllPages: true, columnId: '_checkbox_selector', cssClass: null, field: 'sel', @@ -135,6 +143,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => { expect(plugin).toBeTruthy(); expect(plugin.addonOptions).toEqual({ + applySelectOnAllPages: true, columnId: '_checkbox_selector', cssClass: 'some-class', field: 'sel', @@ -184,7 +193,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => { expect(setSelectedRowSpy).not.toHaveBeenCalled(); }); - it('should create the plugin and expect "setSelectedRows" to called with all rows toggling to be selected', () => { + it('should create the plugin and expect "setSelectedRows" to called with all rows toggling to be selected when "applySelectOnAllPages" is disabled', () => { jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(false); jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); jest.spyOn(gridStub, 'getDataLength').mockReturnValue(3); @@ -198,7 +207,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => { plugin.selectedRowsLookup = { 1: false, 2: true }; plugin.init(gridStub); - plugin.setOptions({ hideInColumnTitleRow: false, hideInFilterHeaderRow: true, hideSelectAllCheckbox: false, onSelectAllToggleStart: onToggleStartMock, onSelectAllToggleEnd: onToggleEndMock }); + plugin.setOptions({ applySelectOnAllPages: false, hideInColumnTitleRow: false, hideInFilterHeaderRow: true, hideSelectAllCheckbox: false, onSelectAllToggleStart: onToggleStartMock, onSelectAllToggleEnd: onToggleEndMock }); const checkboxElm = document.createElement('input'); checkboxElm.type = 'checkbox'; @@ -214,7 +223,41 @@ describe('SlickCheckboxSelectColumn Plugin', () => { expect(setSelectedRowSpy).toHaveBeenCalledWith([0, 1, 2], 'click.selectAll'); expect(onToggleStartMock).toHaveBeenCalledWith(expect.anything(), { caller: 'click.selectAll', previousSelectedRows: undefined, }); expect(onToggleEndMock).toHaveBeenCalledWith(expect.anything(), { caller: 'click.selectAll', previousSelectedRows: undefined, rows: [0, 2] }); + }); + it('should create the plugin and expect "setSelectedRows" to called with all rows toggling to be selected when "applySelectOnAllPages" is enabled', () => { + jest.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(false); + jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); + jest.spyOn(gridStub, 'getDataLength').mockReturnValue(3); + jest.spyOn(gridStub, 'getDataItem') + .mockReturnValue({ firstName: 'John', lastName: 'Doe', age: 30 }) + .mockReturnValueOnce({ firstName: 'Jane', lastName: 'Doe', age: 28 }) + .mockReturnValueOnce({ __group: true, __groupTotals: { age: { sum: 58 } } }); + jest.spyOn(dataViewStub, 'getFilteredItems').mockReturnValue([{ id: 22, firstName: 'John', lastName: 'Doe', age: 30 }]); + const setSelectedRowSpy = jest.spyOn(gridStub, 'setSelectedRows'); + const onToggleEndMock = jest.fn(); + const onToggleStartMock = jest.fn(); + const setSelectedIdsSpy = jest.spyOn(dataViewStub, 'setSelectedIds'); + + plugin.selectedRowsLookup = { 1: false, 2: true }; + plugin.init(gridStub); + plugin.setOptions({ applySelectOnAllPages: true, hideInColumnTitleRow: false, hideInFilterHeaderRow: true, hideSelectAllCheckbox: false, onSelectAllToggleStart: onToggleStartMock, onSelectAllToggleEnd: onToggleEndMock }); + + const checkboxElm = document.createElement('input'); + checkboxElm.type = 'checkbox'; + checkboxElm.checked = true; + const clickEvent = addJQueryEventPropagation(new Event('click'), '', '', checkboxElm); + const stopPropagationSpy = jest.spyOn(clickEvent, 'stopPropagation'); + const stopImmediatePropagationSpy = jest.spyOn(clickEvent, 'stopImmediatePropagation'); + gridStub.onHeaderClick.notify({ column: { id: '_checkbox_selector', field: 'sel' }, grid: gridStub }, clickEvent); + + expect(plugin).toBeTruthy(); + expect(stopPropagationSpy).toHaveBeenCalled(); + expect(stopImmediatePropagationSpy).toHaveBeenCalled(); + expect(setSelectedRowSpy).toHaveBeenCalledWith([0, 1, 2], 'click.selectAll'); + expect(onToggleStartMock).toHaveBeenCalledWith(expect.anything(), { caller: 'click.selectAll', previousSelectedRows: undefined, }); + expect(onToggleEndMock).toHaveBeenCalledWith(expect.anything(), { caller: 'click.selectAll', previousSelectedRows: undefined, rows: [0, 2] }); + expect(setSelectedIdsSpy).toHaveBeenCalledWith([22], { isRowBeingAdded: true }); }); it('should create the plugin and call "setOptions" and expect options changed and hide both Select All toggle when setting "hideSelectAllCheckbox: false" and "hideInColumnTitleRow: true"', () => { @@ -237,6 +280,17 @@ describe('SlickCheckboxSelectColumn Plugin', () => { expect(filterSelectAll.style.display).toEqual('none'); }); + it('should create the plugin and and expect it to automatically disable "applySelectOnAllPages" when the BackendServiceApi is used', () => { + const nodeElm = document.createElement('div'); + nodeElm.className = 'slick-headerrow-column'; + jest.spyOn(gridStub, 'getOptions').mockReturnValue({ backendServiceApi: {} as any }); + + plugin = new SlickCheckboxSelectColumn(pubSubServiceStub); + plugin.init(gridStub); + + expect(plugin.getOptions()).toEqual(expect.objectContaining({ applySelectOnAllPages: false })); + }); + it('should call "deSelectRows" and expect "setSelectedRows" to be called with only the rows that are found in selectable lookup', () => { jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue([1, 2]); const setSelectedRowSpy = jest.spyOn(gridStub, 'setSelectedRows'); @@ -352,7 +406,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => { const nodeElm = document.createElement('div'); nodeElm.className = 'slick-headerrow-column'; - plugin = new SlickCheckboxSelectColumn(pubSubServiceStub, { hideInFilterHeaderRow: false, }); + plugin = new SlickCheckboxSelectColumn(pubSubServiceStub, { applySelectOnAllPages: false, hideInFilterHeaderRow: false, }); plugin.init(gridStub); gridStub.onHeaderRowCellRendered.notify({ column: { id: '_checkbox_selector', field: 'sel' }, node: nodeElm, grid: gridStub }); @@ -555,14 +609,40 @@ describe('SlickCheckboxSelectColumn Plugin', () => { expect(stopImmediatePropagationSpy).toHaveBeenCalled(); }); - it('should trigger "onSelectedRowsChanged" event and invalidate row and render to be called but without "setSelectedRows" when checkSelectableOverride returns True or not provided', () => { + it('should trigger "onSelectedRowsChanged" event and invalidate row and render to be called but without "setSelectedRows" when "applySelectOnAllPages" is disabled & checkSelectableOverride returns True or is not provided', () => { const invalidateRowSpy = jest.spyOn(gridStub, 'invalidateRow'); const renderSpy = jest.spyOn(gridStub, 'render'); const updateColumnHeaderSpy = jest.spyOn(gridStub, 'updateColumnHeader'); const setSelectedRowSpy = jest.spyOn(gridStub, 'setSelectedRows'); jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); - plugin = new SlickCheckboxSelectColumn(pubSubServiceStub, { hideInColumnTitleRow: false, hideSelectAllCheckbox: false }); + plugin = new SlickCheckboxSelectColumn(pubSubServiceStub, { applySelectOnAllPages: false, hideInColumnTitleRow: false, hideSelectAllCheckbox: false }); + plugin.init(gridStub); + const checkboxElm = document.createElement('input'); + checkboxElm.type = 'checkbox'; + const clickEvent = addJQueryEventPropagation(new Event('keyDown'), '', ' ', checkboxElm); + gridStub.onSelectedRowsChanged.notify({ rows: [2, 3], previousSelectedRows: [0, 1], grid: gridStub } as OnSelectedRowsChangedEventArgs, clickEvent); + + expect(plugin).toBeTruthy(); + expect(invalidateRowSpy).toHaveBeenCalled(); + expect(renderSpy).toHaveBeenCalled(); + expect(setSelectedRowSpy).not.toHaveBeenCalled(); + expect(updateColumnHeaderSpy).toHaveBeenCalledWith( + '_checkbox_selector', + ``, + 'Select/Deselect All' + ); + }); + + it('should trigger "onSelectedRowsChanged" event and invalidate row and render to be called but without "setSelectedRows" when we are not using a DataView & checkSelectableOverride returns True or is not provided', () => { + const invalidateRowSpy = jest.spyOn(gridStub, 'invalidateRow'); + const renderSpy = jest.spyOn(gridStub, 'render'); + const updateColumnHeaderSpy = jest.spyOn(gridStub, 'updateColumnHeader'); + const setSelectedRowSpy = jest.spyOn(gridStub, 'setSelectedRows'); + jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); + jest.spyOn(gridStub, 'getData').mockReturnValueOnce([]); + + plugin = new SlickCheckboxSelectColumn(pubSubServiceStub, { applySelectOnAllPages: true, hideInColumnTitleRow: false, hideSelectAllCheckbox: false }); plugin.init(gridStub); const checkboxElm = document.createElement('input'); checkboxElm.type = 'checkbox'; @@ -589,7 +669,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => { const setSelectedRowSpy = jest.spyOn(gridStub, 'setSelectedRows'); jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); - plugin = new SlickCheckboxSelectColumn(pubSubServiceStub, { hideInFilterHeaderRow: false, hideSelectAllCheckbox: false, selectableOverride: () => false }); + plugin = new SlickCheckboxSelectColumn(pubSubServiceStub, { applySelectOnAllPages: false, hideInFilterHeaderRow: false, hideSelectAllCheckbox: false, selectableOverride: () => false }); plugin.init(gridStub); plugin.selectedRowsLookup = { 1: false, 2: true }; @@ -610,4 +690,36 @@ describe('SlickCheckboxSelectColumn Plugin', () => { 'Select/Deselect All' ); }); + + it('should trigger "onSelectedRowIdsChanged" event and invalidate row and render to be called also with "setSelectedRows" when checkSelectableOverride returns False and input select checkbox is all checked', () => { + const nodeElm = document.createElement('div'); + nodeElm.className = 'slick-headerrow-column'; + const invalidateRowSpy = jest.spyOn(gridStub, 'invalidateRow'); + const renderSpy = jest.spyOn(gridStub, 'render'); + const updateColumnHeaderSpy = jest.spyOn(gridStub, 'updateColumnHeader'); + const setSelectedRowSpy = jest.spyOn(gridStub, 'setSelectedRows'); + jest.spyOn(dataViewStub, 'getAllSelectedFilteredIds').mockReturnValueOnce([1, 2]); + jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); + jest.spyOn(dataViewStub, 'getFilteredItems').mockReturnValueOnce([{ id: 22, firstName: 'John', lastName: 'Doe', age: 30 }]); + jest.spyOn(dataViewStub, 'getItemCount').mockReturnValueOnce(2); + jest.spyOn(dataViewStub, 'getItemByIdx').mockReturnValueOnce({ id: 22, firstName: 'John', lastName: 'Doe', age: 30 }); + + plugin = new SlickCheckboxSelectColumn(pubSubServiceStub, { hideInFilterHeaderRow: false, hideSelectAllCheckbox: false, selectableOverride: () => false }); + plugin.init(gridStub); + plugin.selectedRowsLookup = { 1: false, 2: true }; + + gridStub.onHeaderRowCellRendered.notify({ column: { id: '_checkbox_selector', field: 'sel' }, node: nodeElm, grid: gridStub }); + + const checkboxElm = document.createElement('input'); + checkboxElm.type = 'checkbox'; + const clickEvent = addJQueryEventPropagation(new Event('keyDown'), '', ' ', checkboxElm); + dataViewStub.onSelectedRowIdsChanged.notify({ rows: [0, 1], filteredIds: [1, 2], ids: [1, 2], selectedRowIds: [1, 2], dataView: dataViewStub, grid: gridStub }, clickEvent); + + expect(plugin).toBeTruthy(); + expect(updateColumnHeaderSpy).toHaveBeenCalledWith( + '_checkbox_selector', + ``, + 'Select/Deselect All' + ); + }); }); diff --git a/packages/common/src/extensions/slickCheckboxSelectColumn.ts b/packages/common/src/extensions/slickCheckboxSelectColumn.ts index 92691e674..ab9553e90 100644 --- a/packages/common/src/extensions/slickCheckboxSelectColumn.ts +++ b/packages/common/src/extensions/slickCheckboxSelectColumn.ts @@ -1,7 +1,7 @@ import { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; import { KeyCode } from '../enums/keyCode.enum'; -import { CheckboxSelectorOption, Column, DOMMouseOrTouchEvent, GridOption, SelectableOverrideCallback, SlickEventData, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index'; +import { CheckboxSelectorOption, Column, DOMMouseOrTouchEvent, GridOption, SelectableOverrideCallback, SlickDataView, SlickEventData, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index'; import { SlickRowSelectionModel } from './slickRowSelectionModel'; import { createDomElement, emptyElement } from '../services/domUtilities'; import { BindingEventService } from '../services/bindingEvent.service'; @@ -18,16 +18,19 @@ export class SlickCheckboxSelectColumn { hideSelectAllCheckbox: false, toolTip: 'Select/Deselect All', width: 30, + applySelectOnAllPages: true, // when that is enabled the "Select All" will be applied to all pages (when using Pagination) hideInColumnTitleRow: false, hideInFilterHeaderRow: true } as unknown as CheckboxSelectorOption; protected _addonOptions: CheckboxSelectorOption = this._defaults; protected _bindEventService: BindingEventService; protected _checkboxColumnCellIndex: number | null = null; + protected _dataView!: SlickDataView; protected _eventHandler: SlickEventHandler; protected _headerRowNode?: HTMLElement; protected _grid!: SlickGrid; protected _isSelectAllChecked = false; + protected _isUsingDataView = false; protected _rowSelectionModel?: SlickRowSelectionModel; protected _selectableOverride?: SelectableOverrideCallback | number; protected _selectAll_UID: number; @@ -63,11 +66,27 @@ export class SlickCheckboxSelectColumn { init(grid: SlickGrid) { this._grid = grid; + this._isUsingDataView = !Array.isArray(grid.getData()); + if (this._isUsingDataView) { + this._dataView = grid.getData(); + } + + // we cannot apply "Select All" to all pages when using a Backend Service API (OData, GraphQL, ...) + if (this.gridOptions.backendServiceApi) { + this._addonOptions.applySelectOnAllPages = false; + } + this._eventHandler .subscribe(grid.onSelectedRowsChanged, this.handleSelectedRowsChanged.bind(this) as EventListener) .subscribe(grid.onClick, this.handleClick.bind(this) as EventListener) .subscribe(grid.onKeyDown, this.handleKeyDown.bind(this) as EventListener); + if (this._isUsingDataView && this._dataView && this._addonOptions.applySelectOnAllPages) { + this._eventHandler + .subscribe(this._dataView.onSelectedRowIdsChanged, this.handleDataViewSelectedIdsChanged.bind(this) as EventListener) + .subscribe(this._dataView.onPagingInfoChanged, this.handleDataViewSelectedIdsChanged.bind(this) as EventListener); + } + if (!this._addonOptions.hideInFilterHeaderRow) { this.addCheckboxToFilterHeaderRow(grid); } @@ -262,7 +281,7 @@ export class SlickCheckboxSelectColumn { protected addCheckboxToFilterHeaderRow(grid: SlickGrid) { this._eventHandler.subscribe(grid.onHeaderRowCellRendered, (_e: any, args: any) => { - if (args.column.field === (this.addonOptions.field || 'sel')) { + if (args.column.field === (this._addonOptions.field || 'sel')) { emptyElement(args.node); // @@ -313,6 +332,36 @@ export class SlickCheckboxSelectColumn { return this._checkboxColumnCellIndex; } + protected handleDataViewSelectedIdsChanged() { + const selectedIds = this._dataView.getAllSelectedFilteredIds(); + const filteredItems = this._dataView.getFilteredItems(); + let disabledCount = 0; + + if (typeof this._selectableOverride === 'function' && selectedIds.length > 0) { + for (let k = 0; k < this._dataView.getItemCount(); k++) { + // If we are allowed to select the row + const dataItem = this._dataView.getItemByIdx(k); + const idProperty = this._dataView.getIdPropertyName(); + const dataItemId = dataItem[idProperty]; + const foundItemIdx = filteredItems.findIndex((item) => item[idProperty] === dataItemId); + if (foundItemIdx >= 0 && !this.checkSelectableOverride(k, dataItem, this._grid)) { + disabledCount++; + } + } + } + this._isSelectAllChecked = (selectedIds.length + disabledCount) >= filteredItems.length; + + if (!this._addonOptions.hideInColumnTitleRow && !this._addonOptions.hideSelectAllCheckbox) { + this.renderSelectAllCheckbox(this._isSelectAllChecked); + } + if (!this._addonOptions.hideInFilterHeaderRow) { + const selectAllElm = this.headerRowNode?.querySelector(`#header-filter-selector${this._selectAll_UID}`); + if (selectAllElm) { + selectAllElm.checked = this._isSelectAllChecked; + } + } + } + protected handleClick(e: DOMMouseOrTouchEvent, args: { row: number; cell: number; grid: SlickGrid; }) { // clicking on a row select checkbox if (this._grid.getColumns()[args.cell].id === this._addonOptions.columnId && e.target.type === 'checkbox') { @@ -339,8 +388,8 @@ export class SlickCheckboxSelectColumn { } // who called the selection? - const isExecutingSelectAll = e.target.checked; - const caller = isExecutingSelectAll ? 'click.selectAll' : 'click.unselectAll'; + let isAllSelected = e.target.checked; + const caller = isAllSelected ? 'click.selectAll' : 'click.unselectAll'; // trigger event before the real selection so that we have an event before & the next one after the change const previousSelectedRows = this._grid.getSelectedRows(); @@ -351,7 +400,7 @@ export class SlickCheckboxSelectColumn { } let newSelectedRows: number[] = []; // when unselecting all, the array will become empty - if (isExecutingSelectAll) { + if (isAllSelected) { const rows = []; for (let i = 0; i < this._grid.getDataLength(); i++) { // Get the row and check it's a selectable row before pushing it onto the stack @@ -361,6 +410,20 @@ export class SlickCheckboxSelectColumn { } } newSelectedRows = rows; + isAllSelected = true; + } + + if (this._isUsingDataView && this._dataView && this._addonOptions.applySelectOnAllPages) { + const ids = []; + const filteredItems = this._dataView.getFilteredItems(); + for (let j = 0; j < filteredItems.length; j++) { + // Get the row and check it's a selectable ID (it could be in a different page) before pushing it onto the stack + const dataviewRowItem = filteredItems[j]; + if (this.checkSelectableOverride(j, dataviewRowItem, this._grid)) { + ids.push(dataviewRowItem[this._dataView.getIdPropertyName()]); + } + } + this._dataView.setSelectedIds(ids, { isRowBeingAdded: isAllSelected }); } // we finally need to call the actual row selection from SlickGrid method @@ -432,15 +495,18 @@ export class SlickCheckboxSelectColumn { this._grid.render(); this._isSelectAllChecked = (selectedRows?.length ?? 0) + disabledCount >= this._grid.getDataLength(); - if (!this._addonOptions.hideInColumnTitleRow && !this._addonOptions.hideSelectAllCheckbox) { - this.renderSelectAllCheckbox(this._isSelectAllChecked); - } - if (!this._addonOptions.hideInFilterHeaderRow) { - const selectAllElm = this.headerRowNode?.querySelector(`#header-filter-selector${this._selectAll_UID}`); - if (selectAllElm) { - selectAllElm.checked = this._isSelectAllChecked; + if (!this._isUsingDataView || !this._addonOptions.applySelectOnAllPages) { + if (!this._addonOptions.hideInColumnTitleRow && !this._addonOptions.hideSelectAllCheckbox) { + this.renderSelectAllCheckbox(this._isSelectAllChecked); + } + if (!this._addonOptions.hideInFilterHeaderRow) { + const selectAllElm = this.headerRowNode?.querySelector(`#header-filter-selector${this._selectAll_UID}`); + if (selectAllElm) { + selectAllElm.checked = this._isSelectAllChecked; + } } } + // Remove items that shouln't of been selected in the first place (Got here Ctrl + click) if (removeList.length > 0) { for (const itemToRemove of removeList) { diff --git a/packages/common/src/global-grid-options.ts b/packages/common/src/global-grid-options.ts index ec80200e5..977be6af4 100644 --- a/packages/common/src/global-grid-options.ts +++ b/packages/common/src/global-grid-options.ts @@ -98,7 +98,11 @@ export const GlobalGridOptions: GridOption = { maxWidth: 500, }, dataView: { - syncGridSelection: true, // when enabled, this will preserve the row selection even after filtering/sorting/grouping + // when enabled, this will preserve the row selection even after filtering/sorting/grouping + syncGridSelection: { + preserveHidden: false, + preserveHiddenOnSelectionChange: true + }, syncGridSelectionWithBackendService: false, // but disable it when using backend services }, datasetIdPropertyName: 'id', diff --git a/packages/common/src/interfaces/checkboxSelectorOption.interface.ts b/packages/common/src/interfaces/checkboxSelectorOption.interface.ts index e727df404..978cd6fee 100644 --- a/packages/common/src/interfaces/checkboxSelectorOption.interface.ts +++ b/packages/common/src/interfaces/checkboxSelectorOption.interface.ts @@ -1,6 +1,12 @@ import { UsabilityOverrideFn } from '../enums/usabilityOverrideFn.type'; export interface CheckboxSelectorOption { + /** + * Defaults to true, should we apply the row selection on all pages? + * It requires DataView `syncGridSelection` to have `preserveHidden` to be disabled and `preserveHiddenOnSelectionChange` to be enabled. + */ + applySelectOnAllPages?: boolean; + /** Defaults to "_checkbox_selector", you can provide a different column id used as the column header id */ columnId?: string; diff --git a/packages/common/src/interfaces/gridOption.interface.ts b/packages/common/src/interfaces/gridOption.interface.ts index af9c6ca98..6fdfca1ee 100644 --- a/packages/common/src/interfaces/gridOption.interface.ts +++ b/packages/common/src/interfaces/gridOption.interface.ts @@ -178,9 +178,13 @@ export interface GridOption { /** Some of the SlickGrid DataView options */ dataView?: DataViewOption & { /** - * Defaults to true, when using row selection, - * if you don't want the items that are not visible (due to being filtered out or being on a different page) to stay selected, - * then set this property as 'false'. You can also set any of the preserve options instead of a boolean value. + * Wires the grid and the DataView together to keep row selection tied to item ids. + * This is useful since, without it, the grid only knows about rows, so if the items + * move around, the same rows stay selected instead of the selection moving along + * with the items, if you don't want this behavior then set this property as `false`. + * + * You can optionally provide preserve options (object) instead of a boolean value, there are 2 available flag options (preserveHidden, preserveHiddenOnSelectionChange) + * The default Grid Option is to have the flags `preserveHidden` as disabled and `preserveHiddenOnSelectionChange` as enabled. */ syncGridSelection?: boolean | { preserveHidden: boolean; preserveHiddenOnSelectionChange: boolean; }; diff --git a/packages/common/src/interfaces/slickDataView.interface.ts b/packages/common/src/interfaces/slickDataView.interface.ts index a9e615957..9235cd4f1 100644 --- a/packages/common/src/interfaces/slickDataView.interface.ts +++ b/packages/common/src/interfaces/slickDataView.interface.ts @@ -122,8 +122,21 @@ export interface SlickDataView { /** * Returns an array of all item IDs corresponding to the currently selected rows (including non-visible rows). * This will also work with Pagination and will return selected IDs from all pages. + * Note: when using Pagination it will also include hidden selections assuming `preserveHiddenOnSelectionChange` is set to true. */ - getAllSelectedIds(): number[]; + getAllSelectedIds(): Array; + + /** + * Get all selected filtered IDs (similar to "getAllSelectedIds" but only return filtered data) + * Note: when using Pagination it will also include hidden selections assuming `preserveHiddenOnSelectionChange` is set to true. + */ + getAllSelectedFilteredIds(): Array; + + /** + * Get all selected filtered dataContext items (similar to "getAllSelectedItems" but only return filtered data) + * Note: when using Pagination it will also include hidden selections assuming `preserveHiddenOnSelectionChange` is set to true. + */ + getAllSelectedFilteredItems(): T[]; /** * Returns an array of all row dataContext corresponding to the currently selected rows (including non-visible rows). @@ -159,6 +172,9 @@ export interface SlickDataView { // eslint-disable-next-line @typescript-eslint/ban-types setFilter(filterFn: Function): void; + /** Set extra Filter arguments which will be used by the Filter method */ + setFilterArgs(args: any): void; + /** Set the Items with a new Dataset and optionally pass a different Id property name */ setItems(data: any[], objectIdProperty?: string): void; @@ -168,8 +184,17 @@ export interface SlickDataView { /** Set Refresh Hints */ setRefreshHints(hints: any): void; - /** Set extra Filter arguments which will be used by the Filter method */ - setFilterArgs(args: any): void; + /** + * Set current row selected IDs array (regardless of Pagination) + * NOTE: This will NOT change the selection in the grid, if you need to do that then you still need to call + * "grid.setSelectedRows(rows)" + * @param {Array} selectedIds - list of IDs which have been selected for this action + * @param {Object} options + * - `isRowBeingAdded`: defaults to true, are the new selected IDs being added (or removed) as new row selections + * - `shouldTriggerEvent`: defaults to true, should we trigger `onSelectedRowIdsChanged` event + * - `applyRowSelectionToGrid`: defaults to true, should we apply the row selections to the grid in the UI + */ + setSelectedIds(selectedIds: Array, options?: { isRowBeingAdded?: boolean; shouldTriggerEvent?: boolean; applyRowSelectionToGrid?: boolean; }): void; /** Sort Method to use by the DataView */ // eslint-disable-next-line @typescript-eslint/ban-types @@ -237,9 +262,16 @@ export interface SlickDataView { /** Event triggered when any of the row got changed */ onRowsChanged: SlickEvent; - /** Event triggered when the DataView row count changes OR any of the row got changed */ + /** Event triggered when the DataView row count changes OR any of the row got changed */ onRowsOrCountChanged: SlickEvent; + /** + * Event triggered when we changed row selections, + * NOTE: it will trigger an event when changing page because its `rows` might have changed. + * Also note that this event will only work when "syncGridSelection" is enabled + */ + onSelectedRowIdsChanged: SlickEvent; + /** Event triggered when "setItems" function is called */ onSetItemsCalled: SlickEvent; } @@ -249,4 +281,5 @@ export interface OnGroupCollapsedEventArgs { level: number; groupingKey: string export interface OnRowCountChangedEventArgs { previous: number; current: number; itemCount: number; dataView: SlickDataView; callingOnRowsChanged: boolean; } export interface OnRowsChangedEventArgs { rows: number[]; itemCount: number; dataView: SlickDataView; calledOnRowCountChanged: boolean; } export interface OnRowsOrCountChangedEventArgs { rowsDiff: number[]; previousRowCount: number; currentRowCount: number; itemCount: number; rowCountChanged: boolean; rowsChanged: boolean; dataView: SlickDataView; } +export interface OnSelectedRowIdsChangedEventArgs { grid: SlickGrid; added?: boolean; filteredIds: Array; selectedRowIds: Array; ids: Array; rows: number[]; dataView: SlickDataView; } export interface OnSetItemsCalledEventArgs { idProperty: string; itemCount: number; } diff --git a/packages/common/src/services/__tests__/gridState.service.spec.ts b/packages/common/src/services/__tests__/gridState.service.spec.ts index b6ca71191..8470f4f9f 100644 --- a/packages/common/src/services/__tests__/gridState.service.spec.ts +++ b/packages/common/src/services/__tests__/gridState.service.spec.ts @@ -24,7 +24,6 @@ import { GridState, RowDetailView, RowMoveManager, - SlickColumnPicker, SlickGrid, SlickNamespace, SlickRowSelectionModel, @@ -60,11 +59,14 @@ const backendServiceStub = { } as BackendService; const dataViewStub = { + getAllSelectedIds: jest.fn(), + getAllSelectedFilteredIds: jest.fn(), getFilteredItems: jest.fn(), mapIdsToRows: jest.fn(), mapRowsToIds: jest.fn(), onBeforePagingInfoChanged: new Slick.Event(), onPagingInfoChanged: new Slick.Event(), + onSelectedRowIdsChanged: new Slick.Event(), } as unknown as SlickDataView; const gridStub = { @@ -526,73 +528,23 @@ describe('GridStateService', () => { it('should call the "mapIdsToRows" from the DataView and get the data IDs from the "selectedRowDataContextIds" array', () => { const mockRowIndexes = [3, 44]; const mockRowIds = [333, 444]; - const mockRowItems = [{ id: 333 }, { id: 444 }]; - const gridOptionsMock = { enablePagination: true, enableRowSelection: true } as GridOption; - jest.spyOn(dataViewStub, 'getFilteredItems').mockReturnValue(mockRowItems); - jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); - const mapIdSpy = jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue(mockRowIndexes); + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue(mockRowIds); + jest.spyOn(dataViewStub, 'getAllSelectedFilteredIds').mockReturnValue(mockRowIds); + jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue(mockRowIndexes); service.selectedRowDataContextIds = mockRowIds; const output = service.getCurrentRowSelections(); - expect(mapIdSpy).toHaveBeenCalled(); expect(output).toEqual({ gridRowIndexes: mockRowIndexes, dataContextIds: mockRowIds, filteredDataContextIds: mockRowIds }); }); }); describe('Row Selection - bindSlickGridRowSelectionToGridStateChange method', () => { - let pinningMock: CurrentPinning; - let gridOptionsMock: GridOption; - beforeEach(() => { jest.clearAllMocks(); }); - describe('without Pagination', () => { - beforeEach(() => { - pinningMock = { frozenBottom: false, frozenColumn: -1, frozenRow: -1 } as CurrentPinning; - gridOptionsMock = { enablePagination: false, enableRowSelection: true, ...pinningMock } as GridOption; - jest.spyOn(gridStub, 'getOptions').mockReturnValue(gridOptionsMock); - }); - - it('should call the "onGridStateChanged" event with the row selection when Pagination is disabled and "onSelectedRowsChanged" is triggered', (done) => { - const mockRowIndexes = [3, 44]; - const mockRowIds = [333, 444]; - const mockRowItems = [{ id: 333 }, { id: 444 }]; - const pubSubSpy = jest.spyOn(mockPubSub, 'publish'); - const columnMock = [{ columnId: 'field1', cssClass: 'red', headerCssClass: '', width: 100 }] as CurrentColumn[]; - const filterMock = [{ columnId: 'field1', operator: 'EQ', searchTerms: [] }] as CurrentFilter[]; - const sorterMock = [{ columnId: 'field1', direction: 'ASC' }, { columnId: 'field2', direction: 'DESC' }] as CurrentSorter[]; - - jest.spyOn(dataViewStub, 'getFilteredItems').mockReturnValue(mockRowItems); - jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue(mockRowIndexes); - const mapRowsSpy = jest.spyOn(dataViewStub, 'mapRowsToIds').mockReturnValue(mockRowIds); - jest.spyOn(service, 'getCurrentColumns').mockReturnValue(columnMock); - jest.spyOn(service, 'getCurrentFilters').mockReturnValue(filterMock); - jest.spyOn(service, 'getCurrentSorters').mockReturnValue(sorterMock); - - service.init(gridStub); - service.selectedRowDataContextIds = mockRowIds; - gridStub.onSelectedRowsChanged.notify({ rows: mockRowIndexes, previousSelectedRows: [], grid: gridStub }); - - setTimeout(() => { - expect(mapRowsSpy).toHaveBeenCalled(); - expect(pubSubSpy).toHaveBeenLastCalledWith(`onGridStateChanged`, { - change: { newValues: { gridRowIndexes: mockRowIndexes, dataContextIds: mockRowIds, filteredDataContextIds: mockRowIds }, type: 'rowSelection', }, - gridState: { - columns: columnMock, - filters: filterMock, - sorters: sorterMock, - pinning: pinningMock, - rowSelection: { gridRowIndexes: mockRowIndexes, dataContextIds: mockRowIds, filteredDataContextIds: mockRowIds }, - }, - }); - done(); - }); - }); - }); - - describe('with Pagination (bindSlickGridRowSelectionWithPaginationToGridStateChange)', () => { + describe('with Pagination', () => { let pinningMock: CurrentPinning; beforeEach(() => { jest.clearAllMocks(); @@ -603,235 +555,43 @@ describe('GridStateService', () => { jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); }); - it('should call the "onGridStateChanged" event with the row selection when Pagination is disabled and "onSelectedRowsChanged" is triggered', (done) => { - const mockPreviousRowIndexes = [3, 33]; - const mockRowIndexes = [3, 44]; - const mockRowIds = [333, 444]; - const mockRowItems = [{ id: 333 }, { id: 444 }]; - const columnMock = [{ columnId: 'field1', cssClass: 'red', headerCssClass: '', width: 100 }] as CurrentColumn[]; - const filterMock = [{ columnId: 'field1', operator: 'EQ', searchTerms: [] }] as CurrentFilter[]; - const sorterMock = [{ columnId: 'field1', direction: 'ASC' }, { columnId: 'field2', direction: 'DESC' }] as CurrentSorter[]; - const paginationMock = { pageNumber: 3, pageSize: 25 } as CurrentPagination; - - jest.spyOn(dataViewStub, 'getFilteredItems').mockReturnValue(mockRowItems); - jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue(mockRowIndexes); - jest.spyOn(service, 'getCurrentColumns').mockReturnValue(columnMock); - jest.spyOn(service, 'getCurrentFilters').mockReturnValue(filterMock); - jest.spyOn(service, 'getCurrentSorters').mockReturnValue(sorterMock); - jest.spyOn(service, 'getCurrentPagination').mockReturnValue(paginationMock); - const pubSubSpy = jest.spyOn(mockPubSub, 'publish'); - const mapRowsSpy = jest.spyOn(dataViewStub, 'mapRowsToIds').mockReturnValue(mockRowIds); - - service.init(gridStub); - service.selectedRowDataContextIds = mockRowIds; - - // the regular event flow is 1.onBeforePagingInfoChanged, 2.onPagingInfoChanged then 3.onSelectedRowsChanged - dataViewStub.onBeforePagingInfoChanged.notify({ pageSize: paginationMock.pageSize, pageNum: 0, totalPages: 1, totalRows: 0, dataView: dataViewStub }); - dataViewStub.onPagingInfoChanged.notify({ pageSize: paginationMock.pageSize, pageNum: (paginationMock.pageNumber - 1), totalPages: 1, totalRows: 0, dataView: dataViewStub }); - gridStub.onSelectedRowsChanged.notify({ rows: mockRowIndexes, previousSelectedRows: mockPreviousRowIndexes, grid: gridStub }); - - setTimeout(() => { - expect(mapRowsSpy).toHaveBeenCalled(); - expect(pubSubSpy).toHaveBeenLastCalledWith(`onGridStateChanged`, { - change: { newValues: { gridRowIndexes: mockRowIndexes, dataContextIds: mockRowIds, filteredDataContextIds: mockRowIds }, type: 'rowSelection' }, - gridState: { - columns: columnMock, - filters: filterMock, - sorters: sorterMock, - pinning: pinningMock, - pagination: paginationMock, - rowSelection: { gridRowIndexes: mockRowIndexes, dataContextIds: mockRowIds, filteredDataContextIds: mockRowIds }, - }, - }); - done(); - }); - }); - - it('should call the "setSelectedRows" grid method inside the "onPagingInfoChanged" event when the rows are not yet selected in the grid', (done) => { - const currentSelectedRowIndexes = [4, 44]; - const shouldBeSelectedRowIndexes = [3, 44]; - const mockRowIds = [333, 444]; - const columnMock = [{ columnId: 'field1', cssClass: 'red', headerCssClass: '', width: 100 }] as CurrentColumn[]; - const paginationMock = { pageNumber: 3, pageSize: 25 } as CurrentPagination; - - jest.spyOn(service, 'getCurrentColumns').mockReturnValue(columnMock); - jest.spyOn(service, 'getCurrentPagination').mockReturnValue(paginationMock); - const setSelectSpy = jest.spyOn(gridStub, 'setSelectedRows'); - - service.init(gridStub); - service.selectedRowDataContextIds = mockRowIds; - - // this comparison which has different arrays, will trigger the expectation we're looking for - jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue(shouldBeSelectedRowIndexes); - jest.spyOn(gridStub, 'getSelectedRows').mockReturnValueOnce(currentSelectedRowIndexes); - - // the regular event flow is 1.onBeforePagingInfoChanged, 2.onPagingInfoChanged then 3.onSelectedRowsChanged - dataViewStub.onBeforePagingInfoChanged.notify({ pageSize: paginationMock.pageSize, pageNum: 0, totalPages: 1, totalRows: 0, dataView: dataViewStub }); - dataViewStub.onPagingInfoChanged.notify({ pageSize: paginationMock.pageSize, pageNum: (paginationMock.pageNumber - 1), totalPages: 1, totalRows: 0, dataView: dataViewStub }); - - setTimeout(() => { - expect(setSelectSpy).toHaveBeenCalledWith(shouldBeSelectedRowIndexes); - done(); - }); - }); - - it('should call the "setSelectedRows" grid method inside the "onSelectedRowsChanged" when the rows are not yet selected in the grid before calling "onGridStateChanged" event', (done) => { - const mockPreviousRowIndexes = [3, 33]; - const mockRowIndexes = [3, 44]; - const currentSelectedRowIndexes = [4, 44]; - const shouldBeSelectedRowIndexes = [3, 44]; - const mockRowIds = [333, 444]; - const mockRowItems = [{ id: 333 }, { id: 444 }]; - const columnMock = [{ columnId: 'field1', cssClass: 'red', headerCssClass: '', width: 100 }] as CurrentColumn[]; - const paginationMock = { pageNumber: 3, pageSize: 25 } as CurrentPagination; - - jest.spyOn(dataViewStub, 'getFilteredItems').mockReturnValue(mockRowItems); - jest.spyOn(service, 'getCurrentColumns').mockReturnValue(columnMock); - jest.spyOn(service, 'getCurrentPagination').mockReturnValue(paginationMock); - const pubSubSpy = jest.spyOn(mockPubSub, 'publish'); - const mapRowsSpy = jest.spyOn(dataViewStub, 'mapRowsToIds').mockReturnValue(mockRowIds); - const setSelectSpy = jest.spyOn(gridStub, 'setSelectedRows'); - - service.init(gridStub); - service.selectedRowDataContextIds = mockRowIds; - - // this comparison which has different arrays, will trigger the expectation we're looking for - jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue(shouldBeSelectedRowIndexes); - const getSelectSpy = jest.spyOn(gridStub, 'getSelectedRows').mockReturnValueOnce(currentSelectedRowIndexes).mockReturnValue(shouldBeSelectedRowIndexes); - - // the regular event flow is 1.onBeforePagingInfoChanged, 2.onPagingInfoChanged then 3.onSelectedRowsChanged - dataViewStub.onBeforePagingInfoChanged.notify({ pageSize: paginationMock.pageSize, pageNum: 0, totalPages: 1, totalRows: 0, dataView: dataViewStub }); - // dataViewStub.onPagingInfoChanged.notify({ pageSize: paginationMock.pageSize, pageNum: (paginationMock.pageNumber - 1) }); - gridStub.onSelectedRowsChanged.notify({ rows: mockRowIndexes, previousSelectedRows: mockPreviousRowIndexes, grid: gridStub }); - - setTimeout(() => { - expect(mapRowsSpy).toHaveBeenCalled(); - expect(getSelectSpy).toHaveBeenCalledTimes(2); - expect(setSelectSpy).toHaveBeenCalledWith(shouldBeSelectedRowIndexes); - expect(pubSubSpy).toHaveBeenLastCalledWith(`onGridStateChanged`, { - change: { newValues: { gridRowIndexes: shouldBeSelectedRowIndexes, dataContextIds: mockRowIds, filteredDataContextIds: mockRowIds }, type: 'rowSelection' }, - gridState: { - columns: columnMock, - filters: null, - sorters: null, - pagination: paginationMock, - pinning: pinningMock, - rowSelection: { gridRowIndexes: shouldBeSelectedRowIndexes, dataContextIds: mockRowIds, filteredDataContextIds: mockRowIds }, - }, - }); - done(); - }); - }); - - it('should set new rows in the "selectedRowDataContextIds" setter when "onSelectedRowsChanged" is triggered with new selected row additions', (done) => { - const mockPreviousRowIndexes = [3, 77]; + it('should set new rows in the "selectedRowDataContextIds" setter when "onSelectedRowIdsChanged" is triggered with new selected row additions', (done) => { const mockPreviousDataIds = [333, 777]; const mockNewRowIndexes = [3, 77, 55]; const mockNewDataIds = [333, 777, 555]; const columnMock = [{ columnId: 'field1', cssClass: 'red', headerCssClass: '', width: 100 }] as CurrentColumn[]; const paginationMock = { pageNumber: 3, pageSize: 25 } as CurrentPagination; + const pubSubSpy = jest.spyOn(mockPubSub, 'publish'); jest.spyOn(service, 'getCurrentColumns').mockReturnValue(columnMock); jest.spyOn(service, 'getCurrentPagination').mockReturnValue(paginationMock); - const mapRowsSpy = jest.spyOn(dataViewStub, 'mapRowsToIds').mockReturnValue(mockNewDataIds); - - service.init(gridStub); - service.selectedRowDataContextIds = mockPreviousDataIds; - - // the regular event flow is 1.onBeforePagingInfoChanged, 2.onPagingInfoChanged then 3.onSelectedRowsChanged - dataViewStub.onBeforePagingInfoChanged.notify({ pageSize: paginationMock.pageSize, pageNum: 0, totalPages: 1, totalRows: 0, dataView: dataViewStub }); - dataViewStub.onPagingInfoChanged.notify({ pageSize: paginationMock.pageSize, pageNum: (paginationMock.pageNumber - 1), totalPages: 1, totalRows: 0, dataView: dataViewStub }); - gridStub.onSelectedRowsChanged.notify({ rows: mockNewRowIndexes, previousSelectedRows: mockPreviousRowIndexes, grid: gridStub }); - - setTimeout(() => { - expect(mapRowsSpy).toHaveBeenCalled(); - expect(service.selectedRowDataContextIds).toEqual(mockNewDataIds); - done(); - }); - }); - - it('should set remove some rows (deletions/uncheck) in the "selectedRowDataContextIds" setter when "onSelectedRowsChanged" is triggered with new selected row delitions', (done) => { - const mockPreviousRowIndexes = [3, 77, 55]; - const mockPreviousDataIds = [333, 777, 555]; - const mockNewRowIndexes = [3, 77]; - const mockNewDataIds = [333, 777]; - const columnMock = [{ columnId: 'field1', cssClass: 'red', headerCssClass: '', width: 100 }] as CurrentColumn[]; - const paginationMock = { pageNumber: 3, pageSize: 25 } as CurrentPagination; - - jest.spyOn(service, 'getCurrentColumns').mockReturnValue(columnMock); - jest.spyOn(service, 'getCurrentPagination').mockReturnValue(paginationMock); - const mapRowsSpy = jest.spyOn(dataViewStub, 'mapRowsToIds').mockReturnValue([555]); // remove [555], will remain [333, 777] + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue(mockNewDataIds); + jest.spyOn(dataViewStub, 'getAllSelectedFilteredIds').mockReturnValue(mockNewDataIds); + jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue(mockNewRowIndexes); service.init(gridStub); service.selectedRowDataContextIds = mockPreviousDataIds; - // the regular event flow is 1.onBeforePagingInfoChanged, 2.onPagingInfoChanged then 3.onSelectedRowsChanged - dataViewStub.onBeforePagingInfoChanged.notify({ pageSize: paginationMock.pageSize, pageNum: 0, totalPages: 1, totalRows: 0, dataView: dataViewStub }); - dataViewStub.onPagingInfoChanged.notify({ pageSize: paginationMock.pageSize, pageNum: (paginationMock.pageNumber - 1), totalPages: 1, totalRows: 0, dataView: dataViewStub }); - gridStub.onSelectedRowsChanged.notify({ rows: mockNewRowIndexes, previousSelectedRows: mockPreviousRowIndexes, grid: gridStub }); - gridStub.onSelectedRowsChanged.notify({ rows: mockNewRowIndexes, previousSelectedRows: mockPreviousRowIndexes, grid: gridStub }); + dataViewStub.onSelectedRowIdsChanged.notify({ rows: mockNewRowIndexes, filteredIds: mockNewDataIds, ids: mockNewDataIds, selectedRowIds: mockNewDataIds, dataView: dataViewStub, grid: gridStub }); setTimeout(() => { - expect(mapRowsSpy).toHaveBeenCalled(); expect(service.selectedRowDataContextIds).toEqual(mockNewDataIds); - done(); - }); - }); - - it('should trigger a "onGridStateChanged" event and expect different filtered "filteredDataContextIds" when "onFilterChanged" is triggered with a some data filtered out by the DataView', (done) => { - const mockFullDatasetRowItems = [{ id: 333 }, { id: 444 }, { id: 555 }]; - const mockRowIds = mockFullDatasetRowItems.map((item) => item.id); - - const mockFilteredRowItems = [{ id: 333 }, { id: 555 }]; - const mockFilterSearchTerms = [333, 555]; - const mockRowIndexes = [3, 44]; - const columnMock = [{ columnId: 'field1', cssClass: 'red', headerCssClass: '', width: 100 }] as CurrentColumn[]; - const filterMock = [{ columnId: 'field1', operator: 'EQ', searchTerms: [] }] as CurrentFilter[]; - const sorterMock = [{ columnId: 'field1', direction: 'ASC' }, { columnId: 'field2', direction: 'DESC' }] as CurrentSorter[]; - const paginationMock = { pageNumber: 3, pageSize: 25 } as CurrentPagination; - - jest.spyOn(dataViewStub, 'getFilteredItems').mockReturnValue(mockFilteredRowItems); - jest.spyOn(gridStub, 'getSelectedRows').mockReturnValue(mockRowIndexes); - jest.spyOn(service, 'getCurrentColumns').mockReturnValue(columnMock); - jest.spyOn(service, 'getCurrentFilters').mockReturnValue(filterMock); - jest.spyOn(service, 'getCurrentSorters').mockReturnValue(sorterMock); - jest.spyOn(service, 'getCurrentPagination').mockReturnValue(paginationMock); - const pubSubSpy = jest.spyOn(mockPubSub, 'publish'); - - service.init(gridStub); - service.selectedRowDataContextIds = mockRowIds; - - fnCallbacks['onFilterChanged'](filterMock); - - setTimeout(() => { - expect(pubSubSpy).toBeCalledTimes(2); // 2x for GridState shown below - - // expect filteredDataContextIds to not be changed before the next Grid State change with Row Selection type expect(pubSubSpy).toHaveBeenCalledWith(`onGridStateChanged`, { - change: { newValues: filterMock, type: 'filter' }, + change: { + newValues: { gridRowIndexes: mockNewRowIndexes, dataContextIds: mockNewDataIds, filteredDataContextIds: mockNewDataIds }, + type: GridStateType.rowSelection, + }, gridState: { columns: columnMock, - filters: filterMock, - sorters: sorterMock, + filters: null, + sorters: null, pagination: paginationMock, pinning: pinningMock, - rowSelection: { gridRowIndexes: mockRowIndexes, dataContextIds: mockRowIds, filteredDataContextIds: mockRowIds }, + rowSelection: { gridRowIndexes: mockNewRowIndexes, dataContextIds: mockNewDataIds, filteredDataContextIds: mockNewDataIds }, } }); - // expect filteredDataContextIds to be updated with a Grid State change with Row Selection type - expect(pubSubSpy).toHaveBeenCalledWith(`onGridStateChanged`, { - change: { newValues: { gridRowIndexes: mockRowIndexes, dataContextIds: mockRowIds, filteredDataContextIds: mockFilterSearchTerms }, type: 'rowSelection' }, - gridState: { - columns: columnMock, - filters: filterMock, - sorters: sorterMock, - pagination: paginationMock, - pinning: pinningMock, - rowSelection: { gridRowIndexes: mockRowIndexes, dataContextIds: mockRowIds, filteredDataContextIds: mockFilterSearchTerms }, - }, - }); done(); - }); + }) }); }); }); diff --git a/packages/common/src/services/gridState.service.ts b/packages/common/src/services/gridState.service.ts index a71f693b5..b3574a711 100644 --- a/packages/common/src/services/gridState.service.ts +++ b/packages/common/src/services/gridState.service.ts @@ -33,8 +33,8 @@ export class GridStateService { protected _columns: Column[] = []; protected _grid!: SlickGrid; protected _subscriptions: EventSubscription[] = []; + protected _selectedRowIndexes: number[] | undefined = []; protected _selectedRowDataContextIds: Array | undefined = []; // used with row selection - protected _selectedFilteredRowDataContextIds: Array | undefined = []; // used with row selection protected _wasRecheckedAfterPageChange = true; // used with row selection & pagination constructor( @@ -56,10 +56,6 @@ export class GridStateService { return this._grid?.getOptions?.() ?? {}; } - protected get datasetIdPropName(): string { - return this._gridOptions.datasetIdPropertyName || 'id'; - } - /** Getter of the selected data context object IDs */ get selectedRowDataContextIds(): Array | undefined { return this._selectedRowDataContextIds; @@ -68,9 +64,6 @@ export class GridStateService { /** Setter of the selected data context object IDs */ set selectedRowDataContextIds(dataContextIds: Array | undefined) { this._selectedRowDataContextIds = dataContextIds; - - // since this is coming from a preset, we also need to update the filtered IDs - this._selectedFilteredRowDataContextIds = dataContextIds; } /** @@ -151,7 +144,7 @@ export class GridStateService { * Get the current grid state (filters/sorters/pagination) * @return grid state */ - getCurrentGridState(args?: { requestRefreshRowFilteredRow?: boolean }): GridState { + getCurrentGridState(): GridState { const { frozenColumn, frozenRow, frozenBottom } = this.sharedService.gridOptions; const gridState: GridState = { columns: this.getCurrentColumns(), @@ -168,7 +161,7 @@ export class GridStateService { // optional Row Selection if (this.hasRowSelectionEnabled()) { - const currentRowSelection = this.getCurrentRowSelections(args?.requestRefreshRowFilteredRow); + const currentRowSelection = this.getCurrentRowSelections(); if (currentRowSelection) { gridState.rowSelection = currentRowSelection; } @@ -288,20 +281,13 @@ export class GridStateService { * @param boolean are we requesting a refresh of the Section FilteredRow * @return current row selection */ - getCurrentRowSelections(requestRefreshFilteredRow = true): CurrentRowSelection | null { - if (this._grid && this._gridOptions && this._dataView && this.hasRowSelectionEnabled()) { - let filteredDataContextIds: Array | undefined = []; - const gridRowIndexes: number[] = this._dataView.mapIdsToRows(this._selectedRowDataContextIds || []); // note that this will return only what is visible in current page - const dataContextIds: Array | undefined = this._selectedRowDataContextIds; - - // user might request to refresh the filtered selection dataset - // typically always True, except when "reEvaluateRowSelectionAfterFilterChange" is called and we don't need to refresh the filtered dataset twice - if (requestRefreshFilteredRow === true) { - filteredDataContextIds = this.refreshFilteredRowSelections(); - } - filteredDataContextIds = this._selectedFilteredRowDataContextIds; - - return { gridRowIndexes, dataContextIds, filteredDataContextIds }; + getCurrentRowSelections(): CurrentRowSelection | null { + if (this._grid && this._dataView && this.hasRowSelectionEnabled()) { + return { + gridRowIndexes: this._grid.getSelectedRows() || [], + dataContextIds: this._dataView.getAllSelectedIds() || [], + filteredDataContextIds: this._dataView.getAllSelectedFilteredIds() || [] + }; } return null; } @@ -341,7 +327,7 @@ export class GridStateService { if (typeof syncGridSelection === 'boolean') { preservedRowSelection = this._gridOptions.dataView.syncGridSelection as boolean; } else if (typeof syncGridSelection === 'object') { - preservedRowSelection = syncGridSelection.preserveHidden; + preservedRowSelection = syncGridSelection.preserveHidden || syncGridSelection.preserveHiddenOnSelectionChange; } // if the result is True but the grid is using a Backend Service, we will do an extra flag check the reason is because it might have some unintended behaviors @@ -393,14 +379,10 @@ export class GridStateService { this._subscriptions.push( this.pubSubService.subscribe('onFilterChanged', currentFilters => { this.resetRowSelectionWhenRequired(); - this.pubSubService.publish('onGridStateChanged', { change: { newValues: currentFilters, type: GridStateType.filter }, gridState: this.getCurrentGridState({ requestRefreshRowFilteredRow: !this.hasRowSelectionEnabled() }) }); - - // when Row Selection is enabled, we also need to re-evaluate the row selection with the leftover filtered dataset - if (this.hasRowSelectionEnabled()) { - this.reEvaluateRowSelectionAfterFilterChange(); - } + this.pubSubService.publish('onGridStateChanged', { change: { newValues: currentFilters, type: GridStateType.filter }, gridState: this.getCurrentGridState() }); }) ); + // Subscribe to Event Emitter of Filter cleared this._subscriptions.push( this.pubSubService.subscribe('onFilterCleared', () => { @@ -435,7 +417,17 @@ export class GridStateService { // subscribe to Row Selection changes (when enabled) if (this._gridOptions.enableRowSelection || this._gridOptions.enableCheckboxSelector) { - this.bindSlickGridRowSelectionToGridStateChange(); + this._eventHandler.subscribe(this._dataView.onSelectedRowIdsChanged, (e, args) => { + const previousSelectedRowIndexes = (this._selectedRowIndexes || []).slice(); + const previousSelectedFilteredRowDataContextIds = (this.selectedRowDataContextIds || []).slice(); + this.selectedRowDataContextIds = args.filteredIds; + this._selectedRowIndexes = args.rows; + + if (!dequal(this.selectedRowDataContextIds, previousSelectedFilteredRowDataContextIds) || !dequal(this._selectedRowIndexes, previousSelectedRowIndexes)) { + const newValues = { gridRowIndexes: this._selectedRowIndexes || [], dataContextIds: args.selectedRowIds, filteredDataContextIds: args.filteredIds } as CurrentRowSelection; + this.pubSubService.publish('onGridStateChanged', { change: { newValues, type: GridStateType.rowSelection }, gridState: this.getCurrentGridState() }); + } + }); } // subscribe to HeaderMenu (hide column) @@ -543,112 +535,10 @@ export class GridStateService { }); } - /** - * Bind a Grid Event of Row Selection change to a Grid State change event - * For the row selection, we can't just use the getSelectedRows() since this will only return the visible rows shown in the UI which is not enough. - * The process is much more complex, what we have to do instead is the following - * 1. when changing a row selection, we'll add the new selection if it's not yet in the global array of selected IDs - * 2. when deleting a row selection, we'll remove the selection from our global array of selected IDs (unless it came from a page change) - * 3. if we use Pagination and we change page, we'll keep track with a flag (this flag will be used to skip any deletion when we're changing page) - * 4. after the Page or DataView is changed or updated, we'll do an extra (and delayed) check to make sure that what we have in our global array of selected IDs is displayed on screen - */ - protected bindSlickGridRowSelectionToGridStateChange() { - if (this._grid && this._gridOptions && this._dataView) { - this._eventHandler.subscribe(this._dataView.onBeforePagingInfoChanged, () => { - this._wasRecheckedAfterPageChange = false; // reset the page check flag, to skip deletions on page change (used in code below) - }); - - this._eventHandler.subscribe(this._dataView.onPagingInfoChanged, () => { - // when user changes page, the selected row indexes might not show up - // we can check to make sure it is but it has to be in a delay so it happens after the first "onSelectedRowsChanged" is triggered - setTimeout(() => { - const shouldBeSelectedRowIndexes = this._dataView.mapIdsToRows(this._selectedRowDataContextIds || []); - const currentSelectedRowIndexes = this._grid.getSelectedRows(); - if (!dequal(shouldBeSelectedRowIndexes, currentSelectedRowIndexes)) { - this._grid.setSelectedRows(shouldBeSelectedRowIndexes); - } - }); - }); - - this._eventHandler.subscribe(this._grid.onSelectedRowsChanged, (_e, args) => { - if (Array.isArray(args.rows) && Array.isArray(args.previousSelectedRows)) { - const newSelectedRows = args.rows as number[]; - const prevSelectedRows = args.previousSelectedRows as number[]; - - const newSelectedAdditions = newSelectedRows.filter((i) => prevSelectedRows.indexOf(i) < 0); - const newSelectedDeletions = prevSelectedRows.filter((i) => newSelectedRows.indexOf(i) < 0); - - // deletion might happen when user is changing page, if that is the case then skip the deletion since it's only a visual deletion (current page) - // if it's not a page change (when the flag is true), then proceed with the deletion in our global array of selected IDs - if (this._wasRecheckedAfterPageChange && newSelectedDeletions.length > 0) { - const toDeleteDataIds: Array = this._dataView.mapRowsToIds(newSelectedDeletions) || []; - toDeleteDataIds.forEach((removeId: number | string) => { - if (Array.isArray(this._selectedRowDataContextIds)) { - this._selectedRowDataContextIds.splice((this._selectedRowDataContextIds as Array).indexOf(removeId), 1); - } - }); - } - - // if we have newly added selected row(s), let's update our global array of selected IDs - if (newSelectedAdditions.length > 0) { - const toAddDataIds: Array = this._dataView.mapRowsToIds(newSelectedAdditions) || []; - toAddDataIds.forEach((dataId: number | string) => { - if ((this._selectedRowDataContextIds as Array).indexOf(dataId) === -1) { - (this._selectedRowDataContextIds as Array).push(dataId); - } - }); - } - - // we set this flag which will be used on the 2nd time the "onSelectedRowsChanged" event is called - // when it's the first time, we skip deletion and this is what this flag is for - this._wasRecheckedAfterPageChange = true; - - // form our full selected row IDs, let's make sure these indexes are selected in the grid, if not then let's call a reselect - // this could happen if the previous step was a page change - const shouldBeSelectedRowIndexes = this._dataView.mapIdsToRows(this._selectedRowDataContextIds || []); - const currentSelectedRowIndexes = this._grid.getSelectedRows(); - if (!dequal(shouldBeSelectedRowIndexes, currentSelectedRowIndexes) && this._gridOptions.enablePagination) { - this._grid.setSelectedRows(shouldBeSelectedRowIndexes); - } - - const filteredDataContextIds = this.refreshFilteredRowSelections(); - const newValues = { gridRowIndexes: this._grid.getSelectedRows(), dataContextIds: this._selectedRowDataContextIds, filteredDataContextIds } as CurrentRowSelection; - this.pubSubService.publish('onGridStateChanged', { change: { newValues, type: GridStateType.rowSelection }, gridState: this.getCurrentGridState() }); - } - }); - } - } - /** Check wether the grid has the Row Selection enabled */ protected hasRowSelectionEnabled() { const selectionModel = this._grid.getSelectionModel(); const isRowSelectionEnabled = this._gridOptions.enableRowSelection || this._gridOptions.enableCheckboxSelector; return (isRowSelectionEnabled && selectionModel); } - - protected reEvaluateRowSelectionAfterFilterChange() { - const currentSelectedRowIndexes = this._grid.getSelectedRows(); - const previousSelectedFilteredRowDataContextIds = (this._selectedFilteredRowDataContextIds || []).slice(); - const filteredDataContextIds = this.refreshFilteredRowSelections(); - - // when selection changed, we'll send a Grid State event with the selection changes - if (!dequal(this._selectedFilteredRowDataContextIds, previousSelectedFilteredRowDataContextIds)) { - const newValues = { gridRowIndexes: currentSelectedRowIndexes, dataContextIds: this._selectedRowDataContextIds, filteredDataContextIds } as CurrentRowSelection; - this.pubSubService.publish('onGridStateChanged', { change: { newValues, type: GridStateType.rowSelection }, gridState: this.getCurrentGridState({ requestRefreshRowFilteredRow: false }) }); - } - } - - /** When a Filter is triggered or when user request it, we will refresh the filtered selection array and return it */ - protected refreshFilteredRowSelections(): Array { - let tmpFilteredArray: Array = []; - const filteredDataset = this._dataView.getFilteredItems() || []; - if (Array.isArray(this._selectedRowDataContextIds)) { - const selectedFilteredRowDataContextIds = [...this._selectedRowDataContextIds]; // take a fresh copy of all selections before filtering the row ids - tmpFilteredArray = selectedFilteredRowDataContextIds.filter((selectedRowId: number | string) => { - return filteredDataset.findIndex((item: any) => item[this.datasetIdPropName] === selectedRowId) > -1; - }); - this._selectedFilteredRowDataContextIds = tmpFilteredArray; - } - return tmpFilteredArray; - } } diff --git a/packages/composite-editor-component/src/slick-composite-editor.component.spec.ts b/packages/composite-editor-component/src/slick-composite-editor.component.spec.ts index e918e457c..0494706f1 100644 --- a/packages/composite-editor-component/src/slick-composite-editor.component.spec.ts +++ b/packages/composite-editor-component/src/slick-composite-editor.component.spec.ts @@ -6,7 +6,6 @@ import { Editors, GridOption, GridService, - GridStateService, SlickDataView, SlickGrid, SlickNamespace, @@ -46,11 +45,16 @@ const gridOptionsMock = { } as GridOption; const dataViewStub = { + getAllSelectedIds: jest.fn(), + getAllSelectedFilteredIds: jest.fn(), + getAllSelectedFilteredItems: jest.fn(), getItem: jest.fn(), getItemById: jest.fn(), getItemCount: jest.fn(), getItems: jest.fn(), getLength: jest.fn(), + mapIdsToRows: jest.fn(), + mapRowsToIds: jest.fn(), refresh: jest.fn(), sort: jest.fn(), reSort: jest.fn(), @@ -73,10 +77,6 @@ const gridServiceStub = { updateItems: jest.fn(), } as unknown as GridService; -const gridStateServiceStub = { - getCurrentRowSelections: jest.fn(), -} as unknown as GridStateService; - const gridStub = { autosizeColumns: jest.fn(), editActiveCell: jest.fn(), @@ -142,7 +142,6 @@ describe('CompositeEditorService', () => { beforeEach(() => { container = new ContainerServiceStub(); container.registerInstance('GridService', gridServiceStub); - container.registerInstance('GridStateService', gridStateServiceStub); div = document.createElement('div'); document.body.appendChild(div); Object.defineProperty(document.body, 'innerHeight', { writable: true, configurable: true, value: 1080 }); @@ -171,13 +170,13 @@ describe('CompositeEditorService', () => { gridOptionsMock.enableCellNavigation = true; }); - it('should throw an error when trying to call "init()" method without finding GridService and/or GridStateService from the ContainerService', (done) => { + it('should throw an error when trying to call "init()" method without finding GridService from the ContainerService', (done) => { try { container.registerInstance('GridService', null); component = new SlickCompositeEditorComponent(); component.init(gridStub, container); } catch (e) { - expect(e.toString()).toContain('[Slickgrid-Universal] it seems that the GridService and/or GridStateService are not being loaded properly, make sure the Container Service is properly implemented.'); + expect(e.toString()).toContain('[Slickgrid-Universal] it seems that the GridService is not being loaded properly, make sure the Container Service is properly implemented.'); done(); } }); @@ -763,6 +762,11 @@ describe('CompositeEditorService', () => { }); describe('clone modal type', () => { + beforeEach(() => { + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue([]); + jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue([]); + }); + afterEach(() => { jest.clearAllMocks(); }); @@ -1237,7 +1241,7 @@ describe('CompositeEditorService', () => { component.openDetails({ headerTitle: 'Details' }); component.editors = { zip: mockEditor }; - const zipCol = columnsMock.find(col => col.id === 'zip'); + const zipCol = columnsMock.find(col => col.id === 'zip') as Column; component.changeFormValue(zipCol, 123456); component.changeFormInputValue(zipCol, 123456, true, false); @@ -1283,7 +1287,7 @@ describe('CompositeEditorService', () => { mockEditor.disabled = true; const mockProduct = { id: 222, address: { zip: 123456 }, productName: 'Product ABC', price: 12.55 }; jest.spyOn(gridStub, 'getDataItem').mockReturnValue(mockProduct); - gridOptionsMock.compositeEditorOptions.excludeDisabledFieldFormValues = true; + gridOptionsMock.compositeEditorOptions!.excludeDisabledFieldFormValues = true; component = new SlickCompositeEditorComponent(); component.init(gridStub, container); @@ -1572,7 +1576,8 @@ describe('CompositeEditorService', () => { jest.spyOn(gridStub, 'getDataItem').mockReturnValue(mockProduct); jest.spyOn(gridStub, 'getCellEditor').mockReturnValue(currentEditorMock as any); jest.spyOn(currentEditorMock, 'validate').mockReturnValue({ valid: true, msg: null }); - jest.spyOn(gridStateServiceStub, 'getCurrentRowSelections').mockReturnValue({ gridRowIndexes: [0], dataContextIds: [222] }); + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue([222]); + jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue([0]); jest.spyOn(dataViewStub, 'getItemById').mockReturnValue(mockProduct); const cancelCommitSpy = jest.spyOn(gridStub.getEditController(), 'cancelCurrentEdit'); const setActiveRowSpy = jest.spyOn(gridStub, 'setActiveRow'); @@ -1625,7 +1630,8 @@ describe('CompositeEditorService', () => { jest.spyOn(gridStub, 'getDataItem').mockReturnValue(mockProduct); jest.spyOn(gridStub, 'getCellEditor').mockReturnValue(currentEditorMock as any); jest.spyOn(currentEditorMock, 'validate').mockReturnValue({ valid: true, msg: null }); - jest.spyOn(gridStateServiceStub, 'getCurrentRowSelections').mockReturnValue({ gridRowIndexes: [0], dataContextIds: [222] }); + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue([222]); + jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue([0]); jest.spyOn(dataViewStub, 'getItemById').mockReturnValue(mockProduct); const getEditSpy = jest.spyOn(gridStub, 'getEditController'); const cancelCommitSpy = jest.spyOn(gridStub.getEditController(), 'cancelCurrentEdit'); @@ -1693,7 +1699,8 @@ describe('CompositeEditorService', () => { jest.spyOn(dataViewStub, 'getItems').mockReturnValue([mockProduct1, mockProduct2]); jest.spyOn(gridStub, 'getCellEditor').mockReturnValue(currentEditorMock as any); jest.spyOn(currentEditorMock, 'validate').mockReturnValue({ valid: true, msg: null }); - jest.spyOn(gridStateServiceStub, 'getCurrentRowSelections').mockReturnValue({ gridRowIndexes: [0], dataContextIds: [222] }); + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue([222]); + jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue([0]); const getEditSpy = jest.spyOn(gridStub, 'getEditController'); const cancelCommitSpy = jest.spyOn(gridStub.getEditController(), 'cancelCurrentEdit'); const setActiveCellSpy = jest.spyOn(gridStub, 'setActiveCell'); @@ -1745,7 +1752,8 @@ describe('CompositeEditorService', () => { jest.spyOn(dataViewStub, 'getItems').mockReturnValue([mockProduct1, mockProduct2]); jest.spyOn(gridStub, 'getCellEditor').mockReturnValue(currentEditorMock as any); jest.spyOn(currentEditorMock, 'validate').mockReturnValue({ valid: true, msg: null }); - jest.spyOn(gridStateServiceStub, 'getCurrentRowSelections').mockReturnValue({ gridRowIndexes: [0], dataContextIds: [222] }); + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue([222]); + jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue([0]); const getEditSpy = jest.spyOn(gridStub, 'getEditController'); const cancelCommitSpy = jest.spyOn(gridStub.getEditController(), 'cancelCurrentEdit'); const setActiveCellSpy = jest.spyOn(gridStub, 'setActiveCell'); @@ -1799,7 +1807,8 @@ describe('CompositeEditorService', () => { jest.spyOn(dataViewStub, 'getItems').mockReturnValue([mockProduct1, mockProduct2]); jest.spyOn(gridStub, 'getCellEditor').mockReturnValue(currentEditorMock as any); jest.spyOn(currentEditorMock, 'validate').mockReturnValue({ valid: true, msg: null }); - jest.spyOn(gridStateServiceStub, 'getCurrentRowSelections').mockReturnValue({ gridRowIndexes: [0], dataContextIds: [222] }); + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue([222]); + jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue([0]); const getEditSpy = jest.spyOn(gridStub, 'getEditController'); const cancelCommitSpy = jest.spyOn(gridStub.getEditController(), 'cancelCurrentEdit'); const setActiveCellSpy = jest.spyOn(gridStub, 'setActiveCell'); @@ -1857,7 +1866,8 @@ describe('CompositeEditorService', () => { jest.spyOn(dataViewStub, 'getItems').mockReturnValue([mockProduct1, mockProduct2]); jest.spyOn(gridStub, 'getCellEditor').mockReturnValue(currentEditorMock as any); jest.spyOn(currentEditorMock, 'validate').mockReturnValue({ valid: true, msg: null }); - jest.spyOn(gridStateServiceStub, 'getCurrentRowSelections').mockReturnValue({ gridRowIndexes: [0], dataContextIds: [222] }); + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue([222]); + jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue([0]); const cancelCommitSpy = jest.spyOn(gridStub.getEditController(), 'cancelCurrentEdit'); const mockOnSave = jest.fn(); @@ -1906,7 +1916,6 @@ describe('CompositeEditorService', () => { translateService = new TranslateServiceStub(); translateService.use('fr'); container.registerInstance('GridService', gridServiceStub); - container.registerInstance('GridStateService', gridStateServiceStub); container.registerInstance('TranslaterService', translateService); const newGridOptions = { ...gridOptionsMock, enableRowSelection: true, enableTranslate: true }; @@ -1992,7 +2001,8 @@ describe('CompositeEditorService', () => { jest.spyOn(gridStub, 'getDataItem').mockReturnValue(mockProduct); jest.spyOn(gridStub, 'getCellEditor').mockReturnValue(currentEditorMock as any); jest.spyOn(currentEditorMock, 'validate').mockReturnValue({ valid: true, msg: null }); - jest.spyOn(gridStateServiceStub, 'getCurrentRowSelections').mockReturnValue({ gridRowIndexes: [0], dataContextIds: [222] }); + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue([222]); + jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue([0]); jest.spyOn(dataViewStub, 'getItemById').mockReturnValue(mockProduct); const getEditSpy = jest.spyOn(gridStub, 'getEditController'); const cancelCommitSpy = jest.spyOn(gridStub.getEditController(), 'cancelCurrentEdit'); @@ -2045,7 +2055,8 @@ describe('CompositeEditorService', () => { jest.spyOn(dataViewStub, 'getItems').mockReturnValue([mockProduct1, mockProduct2]); jest.spyOn(gridStub, 'getCellEditor').mockReturnValue(currentEditorMock as any); jest.spyOn(currentEditorMock, 'validate').mockReturnValue({ valid: true, msg: null }); - jest.spyOn(gridStateServiceStub, 'getCurrentRowSelections').mockReturnValue({ gridRowIndexes: [0], dataContextIds: [222] }); + jest.spyOn(dataViewStub, 'getAllSelectedIds').mockReturnValue([222]); + jest.spyOn(dataViewStub, 'mapIdsToRows').mockReturnValue([0]); const mockModalOptions = { headerTitle: 'Details', modalType: 'mass-update' } as CompositeEditorOpenDetailOption; component = new SlickCompositeEditorComponent(); diff --git a/packages/composite-editor-component/src/slick-composite-editor.component.ts b/packages/composite-editor-component/src/slick-composite-editor.component.ts index 9ee44e19b..cc3ec2994 100644 --- a/packages/composite-editor-component/src/slick-composite-editor.component.ts +++ b/packages/composite-editor-component/src/slick-composite-editor.component.ts @@ -9,7 +9,6 @@ import { Constants, ContainerService, createDomElement, - CurrentRowSelection, DOMEvent, Editor, EditorValidationResult, @@ -17,7 +16,6 @@ import { getDescendantProperty, GridOption, GridService, - GridStateService, Locale, numericSortComparer, OnErrorOption, @@ -68,7 +66,6 @@ export class SlickCompositeEditorComponent implements ExternalResource { protected _modalSaveButtonElm!: HTMLButtonElement; protected grid!: SlickGrid; protected gridService: GridService | null = null; - protected gridStateService: GridStateService | null = null; protected translaterService?: TranslaterService | null; get eventHandler(): SlickEventHandler { @@ -112,11 +109,10 @@ export class SlickCompositeEditorComponent implements ExternalResource { init(grid: SlickGrid, containerService: ContainerService) { this.grid = grid; this.gridService = containerService.get('GridService'); - this.gridStateService = containerService.get('GridStateService'); this.translaterService = containerService.get('TranslaterService'); - if (!this.gridService || !this.gridStateService) { - throw new Error('[Slickgrid-Universal] it seems that the GridService and/or GridStateService are not being loaded properly, make sure the Container Service is properly implemented.'); + if (!this.gridService) { + throw new Error('[Slickgrid-Universal] it seems that the GridService is not being loaded properly, make sure the Container Service is properly implemented.'); } if (this.gridOptions.enableTranslate && (!this.translaterService || !this.translaterService.translate)) { @@ -323,8 +319,7 @@ export class SlickCompositeEditorComponent implements ExternalResource { const selectedRowsIndexes = this.hasRowSelectionEnabled() ? this.grid.getSelectedRows() : []; const fullDatasetLength = this.dataView?.getItemCount() ?? 0; this._lastActiveRowNumber = activeRow; - const gridStateSelection = this.gridStateService?.getCurrentRowSelections() as CurrentRowSelection; - const dataContextIds = gridStateSelection?.dataContextIds || []; + const dataContextIds = this.dataView.getAllSelectedIds(); // focus on a first cell with an Editor (unless current cell already has an Editor then do nothing) // also when it's a "Create" modal, we'll scroll to the end of the grid @@ -850,9 +845,8 @@ export class SlickCompositeEditorComponent implements ExternalResource { /** Retrieve the current selection of row indexes & data context Ids */ protected getCurrentRowSelections(): { gridRowIndexes: number[]; dataContextIds: Array; } { - const gridStateSelection = this.gridStateService?.getCurrentRowSelections() as CurrentRowSelection; - const gridRowIndexes = gridStateSelection?.gridRowIndexes || []; - const dataContextIds = gridStateSelection?.dataContextIds || []; + const dataContextIds = this.dataView.getAllSelectedIds(); + const gridRowIndexes = this.dataView.mapIdsToRows(dataContextIds); return { gridRowIndexes, dataContextIds }; } diff --git a/packages/vanilla-bundle/package.json b/packages/vanilla-bundle/package.json index 5874508a2..4c01fd681 100644 --- a/packages/vanilla-bundle/package.json +++ b/packages/vanilla-bundle/package.json @@ -65,7 +65,7 @@ "dequal": "^2.0.3", "flatpickr": "^4.6.13", "jquery": "^3.6.3", - "slickgrid": "^3.0.2", + "slickgrid": "^3.0.3", "sortablejs": "^1.15.0", "whatwg-fetch": "^3.6.2" }, 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 91863fe51..cc75425e2 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 @@ -205,6 +205,7 @@ const mockDataView = { onSetItemsCalled: new MockSlickEvent(), reSort: jest.fn(), setItems: jest.fn(), + setSelectedIds: jest.fn(), syncGridSelection: jest.fn(), } as unknown as SlickDataView; @@ -1658,13 +1659,16 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () }); }); - it('should call trigger a gridStage change and reset selected rows when pagination change is triggered and "enableRowSelection" is set', () => { + it('should trigger a gridStage change and reset selected rows when pagination change is triggered and "enableRowSelection" is set', () => { const mockPagination = { pageNumber: 2, pageSize: 20 } as Pagination; const pluginEaSpy = jest.spyOn(eventPubSubService, 'publish'); const setRowSpy = jest.spyOn(mockGrid, 'setSelectedRows'); jest.spyOn(gridStateServiceStub, 'getCurrentGridState').mockReturnValue({ columns: [], pagination: mockPagination } as GridState); - component.gridOptions = { enableRowSelection: true } as unknown as GridOption; + component.gridOptions = { + enableRowSelection: true, + backendServiceApi: { service: mockGraphqlService as any } + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); component.paginationChanged(mockPagination); @@ -1681,7 +1685,10 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () const setRowSpy = jest.spyOn(mockGrid, 'setSelectedRows'); jest.spyOn(gridStateServiceStub, 'getCurrentGridState').mockReturnValue({ columns: [], pagination: mockPagination } as GridState); - component.gridOptions = { enableCheckboxSelector: true } as unknown as GridOption; + component.gridOptions = { + enableCheckboxSelector: true, + backendServiceApi: { service: mockGraphqlService as any } + } as unknown as GridOption; component.initialization(divContainer, slickEventHandler); component.paginationChanged(mockPagination); @@ -1883,10 +1890,11 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () }); }); - it('should NOT call the "setSelectedRows" when the Grid has Local Pagination and there are row selection presets with "dataContextIds" array set', (done) => { + it('should call the "setSelectedRows" and "setSelectedIds" when the Grid has Local Pagination and there are row selection presets with "dataContextIds" array set', () => { const selectedGridRows = [22]; const mockData = [{ firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Smith' }]; - const selectRowSpy = jest.spyOn(mockGrid, 'setSelectedRows'); + const gridSelectedRowSpy = jest.spyOn(mockGrid, 'setSelectedRows'); + const dvSetSelectedIdSpy = jest.spyOn(mockDataView, 'setSelectedIds'); jest.spyOn(mockGrid, 'getSelectionModel').mockReturnValue(true as any); jest.spyOn(mockDataView, 'getLength').mockReturnValue(mockData.length); @@ -1900,11 +1908,9 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () component.isDatasetInitialized = false; // it won't call the preset unless we reset this flag component.initialization(divContainer, slickEventHandler); - setTimeout(() => { - expect(component.isDatasetInitialized).toBe(true); - expect(selectRowSpy).not.toHaveBeenCalled(); - done(); - }, 2); + expect(component.isDatasetInitialized).toBe(true); + expect(gridSelectedRowSpy).toHaveBeenCalledWith([2]); + expect(dvSetSelectedIdSpy).toHaveBeenCalledWith([22], { applyRowSelectionToGrid: true, isRowBeingAdded: true, shouldTriggerEvent: false }); }); }); 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 e12ede976..c8c0a80c9 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -1,10 +1,10 @@ import { dequal } from 'dequal/lite'; import 'jquery'; import 'flatpickr/dist/l10n/fr'; -import 'slickgrid/dist/slick.core.min'; -import 'slickgrid/dist/slick.interactions.min'; -import 'slickgrid/dist/slick.grid.min'; -import 'slickgrid/dist/slick.dataview.min'; +import 'slickgrid/slick.core'; +import 'slickgrid/slick.interactions'; +import 'slickgrid/slick.grid'; +import 'slickgrid/slick.dataview'; import SortableInstance, * as Sortable_ from 'sortablejs'; const Sortable = ((Sortable_ as any)?.['default'] ?? Sortable_); // patch for rollup @@ -955,11 +955,11 @@ export class SlickVanillaGridBundle { /** * On a Pagination changed, we will trigger a Grid State changed with the new pagination info - * Also if we use Row Selection or the Checkbox Selector, we need to reset any selection + * Also if we use Row Selection or the Checkbox Selector with a Backend Service (Odata, GraphQL), we need to reset any selection */ paginationChanged(pagination: ServicePagination) { const isSyncGridSelectionEnabled = this.gridStateService?.needToPreserveRowSelection() ?? false; - if (this.slickGrid && !isSyncGridSelectionEnabled && (this.gridOptions.enableRowSelection || this.gridOptions.enableCheckboxSelector)) { + if (this.slickGrid && !isSyncGridSelectionEnabled && this._gridOptions?.backendServiceApi && (this.gridOptions.enableRowSelection || this.gridOptions.enableCheckboxSelector)) { this.slickGrid.setSelectedRows([]); } const { pageNumber, pageSize } = pagination; @@ -1284,16 +1284,14 @@ export class SlickVanillaGridBundle { } else if (Array.isArray(gridRowIndexes) && gridRowIndexes.length > 0) { dataContextIds = this.dataView.mapRowsToIds(gridRowIndexes) || []; } - this.gridStateService.selectedRowDataContextIds = dataContextIds; - // change the selected rows except UNLESS it's a Local Grid with Pagination - // local Pagination uses the DataView and that also trigger a change/refresh - // and we don't want to trigger 2 Grid State changes just 1 - if ((this._isLocalGrid && !this.gridOptions.enablePagination) || !this._isLocalGrid) { - setTimeout(() => { - if (this.slickGrid && Array.isArray(gridRowIndexes)) { - this.slickGrid.setSelectedRows(gridRowIndexes); - } + // apply row selection when defined as grid presets + if (this.slickGrid && Array.isArray(gridRowIndexes)) { + this.slickGrid.setSelectedRows(gridRowIndexes); + this.dataView!.setSelectedIds(dataContextIds || [], { + isRowBeingAdded: true, + shouldTriggerEvent: false, // do not trigger when presetting the grid + applyRowSelectionToGrid: true }); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ae7085ed..16f55d3c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ importers: rimraf: ^3.0.2 rxjs: ^7.5.7 serve: ^14.2.0 - slickgrid: ^3.0.2 + slickgrid: ^3.0.3 sortablejs: ^1.15.0 ts-jest: ^29.0.5 ts-node: ^10.9.1 @@ -71,7 +71,7 @@ importers: rimraf: 3.0.2 rxjs: 7.5.7 serve: 14.2.0 - slickgrid: 3.0.2 + slickgrid: 3.0.3 sortablejs: 1.15.0 ts-jest: 29.0.5_jgx6vq7fgl6labivbgpbeuxqt4 ts-node: 10.9.1_bdgp3l2zgaopogaavxusmetvge @@ -202,7 +202,7 @@ importers: postcss-cli: ^10.1.0 rimraf: ^3.0.2 sass: ^1.58.0 - slickgrid: ^3.0.2 + slickgrid: ^3.0.3 sortablejs: ^1.15.0 un-flatten-tree: ^2.0.12 dependencies: @@ -215,7 +215,7 @@ importers: jquery: 3.6.3 moment-mini: 2.29.4 multiple-select-modified: 1.3.17 - slickgrid: 3.0.2 + slickgrid: 3.0.3 sortablejs: 1.15.0 un-flatten-tree: 2.0.12 devDependencies: @@ -453,7 +453,7 @@ importers: jquery: ^3.6.3 npm-run-all2: ^6.0.4 rimraf: ^3.0.2 - slickgrid: ^3.0.2 + slickgrid: ^3.0.3 sortablejs: ^1.15.0 whatwg-fetch: ^3.6.2 dependencies: @@ -467,7 +467,7 @@ importers: dequal: 2.0.3 flatpickr: 4.6.13 jquery: 3.6.3 - slickgrid: 3.0.2 + slickgrid: 3.0.3 sortablejs: 1.15.0 whatwg-fetch: 3.6.2 devDependencies: @@ -9355,8 +9355,8 @@ packages: is-fullwidth-code-point: 3.0.0 dev: true - /slickgrid/3.0.2: - resolution: {integrity: sha512-aLgguzFbw0HSKcSVcSEw+IbZ5qO3NKTvLJtOgF621E2oW/B0Ru+qzWvBjXtlN7bENbp1Lt/lsF5Les4rJHO89w==} + /slickgrid/3.0.3: + resolution: {integrity: sha512-9NlWDTHftNs3+Ta62cF6rV9Vo4PBHCYuBVxb5yHxZ62CiuqtMvKusktEOt1O1RLC1J+lg5v4Qs7LccJ6T3CAJQ==} dependencies: jquery: 3.6.3 sortablejs: 1.15.0 diff --git a/test/cypress.config.ts b/test/cypress.config.ts index bb1e62f37..a3eedfa26 100644 --- a/test/cypress.config.ts +++ b/test/cypress.config.ts @@ -6,7 +6,7 @@ import plugins from './cypress/plugins/index'; export default defineConfig({ video: false, projectId: 'p5zxx6', - viewportWidth: 1000, + viewportWidth: 1200, viewportHeight: 950, fixturesFolder: 'test/cypress/fixtures', screenshotsFolder: 'test/cypress/screenshots', diff --git a/test/cypress/e2e/example01.cy.ts b/test/cypress/e2e/example01.cy.ts index 55eb01f4a..8c5e23bff 100644 --- a/test/cypress/e2e/example01.cy.ts +++ b/test/cypress/e2e/example01.cy.ts @@ -23,13 +23,13 @@ describe('Example 01 - Basic Grids', { retries: 1 }, () => { .should('have.css', 'width', '800px'); cy.get('.grid1 > .slickgrid-container') - .should('have.css', 'height', '225px'); + .should($el => expect(parseInt(`${$el.height()}`, 10)).to.eq(225)); cy.get('.grid2') .should('have.css', 'width', '800px'); cy.get('.grid2 > .slickgrid-container') - .should('have.css', 'height', '255px'); + .should($el => expect(parseInt(`${$el.height()}`, 10)).to.eq(255)); }); it('should have exact column titles on 1st grid', () => { diff --git a/test/cypress/e2e/example09.cy.ts b/test/cypress/e2e/example09.cy.ts index f92cddb6b..52f473630 100644 --- a/test/cypress/e2e/example09.cy.ts +++ b/test/cypress/e2e/example09.cy.ts @@ -726,12 +726,14 @@ describe('Example 09 - OData Grid', { retries: 1 }, () => { .find('.slick-header-left .slick-header-column:nth(1)') .trigger('mouseover') .children('.slick-header-menu-button') - .click(); + .invoke('show') + .click({ force: true }); cy.get('.slick-header-menu') .should('be.visible') .children('.slick-menu-item:nth-of-type(6)') .children('.slick-menu-content') + .invoke('show') .should('contain', 'Remove Filter') .click(); diff --git a/test/cypress/e2e/example10.cy.ts b/test/cypress/e2e/example10.cy.ts index e4da7b825..54fb4d53d 100644 --- a/test/cypress/e2e/example10.cy.ts +++ b/test/cypress/e2e/example10.cy.ts @@ -18,7 +18,7 @@ describe('Example 10 - GraphQL Grid', { retries: 1 }, () => { .should('have.css', 'width', '900px'); cy.get('.grid10 > .slickgrid-container') - .should('have.css', 'height', '275px'); + .should($el => expect(parseInt(`${$el.height()}`)).to.eq(275)); }); it('should have English Text inside some of the Filters', () => { diff --git a/test/cypress/e2e/example11.cy.ts b/test/cypress/e2e/example11.cy.ts index 79e23434d..abfcce639 100644 --- a/test/cypress/e2e/example11.cy.ts +++ b/test/cypress/e2e/example11.cy.ts @@ -29,6 +29,16 @@ describe('Example 11 - Batch Editing', { retries: 1 }, () => { .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); + it('should click on "Clear Local Storage" and expect to be back to original grid with all the columns', () => { + cy.get('[data-test="clear-storage-btn"]') + .click(); + + cy.get('.grid11') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + it('should have "TASK 0" (uppercase) incremented by 1 after each row', () => { cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(1)`).should('contain', 'TASK 0'); cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(1)`).should('contain', 'TASK 1'); @@ -59,9 +69,6 @@ describe('Example 11 - Batch Editing', { retries: 1 }, () => { .should('have.css', 'background-color').and('eq', UNSAVED_RGB_COLOR); cy.get('.editor-duration').type('{esc}'); cy.get('.editor-duration').should('not.exist'); - - cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') - .scrollTo('top'); }); it('should be able to change "Title" values of row indexes 1-3', () => { diff --git a/test/cypress/e2e/example12.cy.ts b/test/cypress/e2e/example12.cy.ts index 775da1da3..745de1f89 100644 --- a/test/cypress/e2e/example12.cy.ts +++ b/test/cypress/e2e/example12.cy.ts @@ -47,17 +47,20 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`) .should('contain', '0 day') .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); cy.get('.editor-duration').type('1').type('{enter}', { force: true }); cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(2)`).should('contain', '1 day') .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); cy.get('.editor-duration').type('2').type('{enter}', { force: true }); cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(2)`).should('contain', '2 days') .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); }); it('should be able to change "Title" values of row indexes 1-3', () => { @@ -67,14 +70,16 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get('.editor-title .editor-footer .btn-save').click(); cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(1)`).should('contain', 'TASK 1111') .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('contain', 'TASK 2').click(); cy.get('.editor-title').type('task 2222'); cy.get('.editor-title .editor-footer .btn-save').click(); cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('contain', 'TASK 2222') .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); }); it('should be able to change "% Complete" values of row indexes 2-4', () => { @@ -83,13 +88,15 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get('.slider-editor input[type=range]').as('range').invoke('val', 5).trigger('change', { force: true }); cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(4)`).should('contain', '5') .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(4)`).click(); cy.get('.slider-editor input[type=range]').as('range').invoke('val', 6).trigger('change', { force: true }); cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(4)`).should('contain', '6') .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); }); it('should not be able to change the "Finish" dates on first 2 rows', () => { @@ -129,19 +136,22 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(8)`).should('contain', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`) .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(8)`).click(); cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(8)`).should('contain', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`) .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(8)`).click(); cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(8)`).should('contain', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`) .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); cy.get('.unsaved-editable-field') .should('have.length', 13); @@ -159,7 +169,8 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(8)`) .should('contain', '') .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); }); it('should undo last edit and expect the date editor to NOT be opened when clicking undo last edit button', () => { @@ -174,7 +185,8 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(8)`) .should('contain', '') .get('.editing-field') - .should('have.css', 'border').and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + .should('have.css', 'border') + .and('contain', `solid ${UNSAVED_RGB_COLOR}`); }); it('should click on the "Save" button and expect 2 console log calls with the queued items & also expect no more unsaved cells', () => { @@ -233,7 +245,7 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { // cy.get('.slick-large-editor-text.editor-title') // .should('have.css', 'border') - // .and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + // .and('contain', `solid ${UNSAVED_RGB_COLOR}`); cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete').as('range').invoke('val', 5).trigger('change', { force: true }); cy.get('.item-details-editor-container .input-group-text').contains('5'); diff --git a/test/cypress/e2e/example14.cy.ts b/test/cypress/e2e/example14.cy.ts index 839734f35..625759b57 100644 --- a/test/cypress/e2e/example14.cy.ts +++ b/test/cypress/e2e/example14.cy.ts @@ -1,4 +1,6 @@ describe('Example 14 - Columns Resize by Content', { retries: 1 }, () => { + const GRID_ROW_HEIGHT = 33; + beforeEach(() => { // create a console.log spy for later use cy.window().then((win) => { @@ -12,7 +14,7 @@ describe('Example 14 - Columns Resize by Content', { retries: 1 }, () => { }); it('should have cell that fit the text content', () => { - cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('equal', 83); + cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('equal', 79); cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('equal', 98); cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('equal', 67); cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('equal', 160); @@ -28,7 +30,7 @@ describe('Example 14 - Columns Resize by Content', { retries: 1 }, () => { it('should make the grid readonly and export to fit the text by content and expect column width to be a bit smaller', () => { cy.get('[data-test="toggle-readonly-btn"]').click(); - cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('equal', 75); + cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('equal', 71); cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('equal', 98); cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('equal', 67); cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('equal', 152); @@ -85,4 +87,101 @@ describe('Example 14 - Columns Resize by Content', { retries: 1 }, () => { cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 120); }); + + it('should change row selection across multiple pages, first page should have 2 selected', () => { + cy.get('[data-test="set-dynamic-rows-btn"]').click(); + + // Row index 3, 4 and 11 (last one will be on 2nd page) + cy.get('input[type="checkbox"]:checked').should('have.length', 2); // 2x in current page and 1x in next page + cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 4}px"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); + }); + + it('should go to next page and expect 1 row selected in that second page', () => { + cy.get('.icon-seek-next').click(); + + cy.get('input[type="checkbox"]:checked').should('have.length', 1); // only 1x row in page 2 + cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); + }); + + it('should click on "Select All" checkbox and expect all rows selected in current page', () => { + const expectedRowIds = [11, 3, 4]; + + // go back to 1st page + cy.get('.icon-seek-prev') + .click(); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .click({ force: true }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(6); + expect(win.console.log).to.be.calledWith('Selected Ids:', expectedRowIds); + }); + }); + + it('should go to the next 2 pages and expect all rows selected in each page', () => { + cy.get('.icon-seek-next') + .click(); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 10); + + cy.get('.icon-seek-next') + .click(); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 10); + }); + + it('should uncheck 1 row and expect current and next page to have "Select All" uncheck', () => { + cy.get('.slick-row:nth(0) .slick-cell:nth(0) input[type=checkbox]') + .click({ force: true }); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('not.be.checked', true); + + cy.get('.icon-seek-next') + .click(); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('not.be.checked', true); + }); + + it('should go back to previous page, select the row that was unchecked and expect "Select All" to be selected again', () => { + cy.get('.icon-seek-prev') + .click(); + + cy.get('.slick-row:nth(0) .slick-cell:nth(0) input[type=checkbox]') + .click({ force: true }); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('be.checked', true); + + cy.get('.icon-seek-next') + .click(); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('be.checked', true); + }); + + it('should Unselect All and expect all pages to no longer have any row selected', () => { + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .click({ force: true }); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 0); + + cy.get('.icon-seek-prev') + .click(); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 0); + + cy.get('.icon-seek-prev') + .click(); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 0); + }); });