diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts index 4cfa45283..24817ff5f 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts @@ -110,7 +110,7 @@ export class Example12 { this._bindingEventService.bind(this.gridContainerElm, 'onbeforeeditcell', this.handleOnBeforeEditCell.bind(this)); this._bindingEventService.bind(this.gridContainerElm, 'oncellchange', this.handleOnCellChange.bind(this)); this._bindingEventService.bind(this.gridContainerElm, 'onclick', this.handleOnCellClicked.bind(this)); - this._bindingEventService.bind(this.gridContainerElm, 'ongridstatechanged', this.handleOnSelectedRowsChanged.bind(this)); + this._bindingEventService.bind(this.gridContainerElm, 'ongridstatechanged', this.handleOnGridStateChanged.bind(this)); this._bindingEventService.bind(this.gridContainerElm, 'ondblclick', () => this.openCompositeModal('edit', 50)); this._bindingEventService.bind(this.gridContainerElm, 'oncompositeeditorchange', this.handleOnCompositeEditorChange.bind(this)); this._bindingEventService.bind(this.gridContainerElm, 'onpaginationchanged', this.handlePaginationChanged.bind(this)); @@ -518,7 +518,8 @@ export class Example12 { this.renderUnsavedStylingOnAllVisibleCells(); } - handleOnSelectedRowsChanged(event) { + handleOnGridStateChanged(event) { + // console.log('handleOnGridStateChanged', event?.detail ?? '') const gridState = event && event.detail && event.detail.gridState; if (Array.isArray(gridState?.rowSelection.dataContextIds)) { this.isMassSelectionDisabled = gridState.rowSelection.dataContextIds.length === 0; diff --git a/package.json b/package.json index 10bdb31be..cfef0b970 100644 --- a/package.json +++ b/package.json @@ -73,4 +73,4 @@ "node": ">=12.0.0", "npm": ">=6.14.0" } -} \ No newline at end of file +} diff --git a/packages/common/package.json b/packages/common/package.json index 27f23413c..a6df9e1e9 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -70,7 +70,7 @@ "lodash.isequal": "^4.5.0", "moment-mini": "^2.24.0", "multiple-select-modified": "^1.3.8", - "slickgrid": "^2.4.31" + "slickgrid": "^2.4.32" }, "devDependencies": { "@types/dompurify": "^2.0.4", diff --git a/packages/common/src/extensions/__tests__/columnPickerExtension.spec.ts b/packages/common/src/extensions/__tests__/columnPickerExtension.spec.ts index e3e3c8879..02ee5dd0e 100644 --- a/packages/common/src/extensions/__tests__/columnPickerExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/columnPickerExtension.spec.ts @@ -9,6 +9,8 @@ declare const Slick: SlickNamespace; const gridStub = { getOptions: jest.fn(), registerPlugin: jest.fn(), + setColumns: jest.fn(), + setOptions: jest.fn(), } as unknown as SlickGrid; const mockAddon = jest.fn().mockImplementation(() => ({ @@ -74,41 +76,59 @@ describe('columnPickerExtension', () => { expect(mockAddon).toHaveBeenCalledWith(columnsMock, gridStub, gridOptionsMock); }); - it('should call internal event handler subscribe and expect the "onColumnSpy" option to be called when addon notify is called', () => { + it('should call internal event handler subscribe and expect the "onColumnsChanged" grid option to be called when addon notify is called', () => { const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.columnPicker as ColumnPicker, 'onColumnsChanged'); const visibleColsSpy = jest.spyOn(SharedService.prototype, 'visibleColumns', 'set'); + const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); const instance = extension.register() as SlickColumnPicker; - instance.onColumnsChanged.notify({ allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }, new Slick.EventData(), gridStub); + instance.onColumnsChanged.notify({ columnId: 'field1', showing: false, allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }, new Slick.EventData(), gridStub); + expect(readjustSpy).not.toHaveBeenCalled(); expect(handlerSpy).toHaveBeenCalledTimes(1); expect(handlerSpy).toHaveBeenCalledWith( { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, expect.anything() ); - expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }); + expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { columnId: 'field1', showing: false, allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }); expect(visibleColsSpy).not.toHaveBeenCalled(); }); - it(`should call internal event handler subscribe and expect the "onColumnSpy" option to be called when addon notify is called + it(`should call internal event handler subscribe and expect the "onColumnsChanged" grid option to be called when addon notify is called and it should override "visibleColumns" when array passed as arguments is bigger than previous visible columns`, () => { const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.columnPicker as ColumnPicker, 'onColumnsChanged'); const visibleColsSpy = jest.spyOn(SharedService.prototype, 'visibleColumns', 'set'); const instance = extension.register() as SlickColumnPicker; - instance.onColumnsChanged.notify({ allColumns: columnsMock, columns: columnsMock, grid: gridStub }, new Slick.EventData(), gridStub); + instance.onColumnsChanged.notify({ columnId: 'field1', showing: true, allColumns: columnsMock, columns: columnsMock, grid: gridStub }, new Slick.EventData(), gridStub); expect(handlerSpy).toHaveBeenCalledTimes(1); expect(handlerSpy).toHaveBeenCalledWith( { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, expect.anything() ); - expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { allColumns: columnsMock, columns: columnsMock, grid: gridStub }); + expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { columnId: 'field1', showing: true, allColumns: columnsMock, columns: columnsMock, grid: gridStub }); expect(visibleColsSpy).toHaveBeenCalledWith(columnsMock); }); + it('should call internal "onColumnsChanged" event and expect "readjustFrozenColumnIndexWhenNeeded" method to be called when the grid is detected to be a frozen grid', () => { + gridOptionsMock.frozenColumn = 0; + const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); + const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); + + const instance = extension.register() as SlickColumnPicker; + instance.onColumnsChanged.notify({ columnId: 'field1', showing: false, allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalledTimes(1); + expect(handlerSpy).toHaveBeenCalledWith( + { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + expect.anything() + ); + expect(readjustSpy).toHaveBeenCalledWith('field1', 0, false, columnsMock, columnsMock.slice(0, 1)); + }); + it('should dispose of the addon', () => { const instance = extension.register() as SlickColumnPicker; const destroySpy = jest.spyOn(instance, 'destroy'); diff --git a/packages/common/src/extensions/__tests__/extensionUtility.spec.ts b/packages/common/src/extensions/__tests__/extensionUtility.spec.ts index fdf3364d6..e95d09787 100644 --- a/packages/common/src/extensions/__tests__/extensionUtility.spec.ts +++ b/packages/common/src/extensions/__tests__/extensionUtility.spec.ts @@ -1,9 +1,16 @@ import { ExtensionName } from '../../enums/index'; -import { GridOption } from '../../interfaces/index'; +import { Column, GridOption, SlickGrid } from '../../interfaces/index'; import { ExtensionUtility } from '../extensionUtility'; import { SharedService } from '../../services/shared.service'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; +const gridStub = { + getOptions: jest.fn(), + setColumns: jest.fn(), + setOptions: jest.fn(), + registerPlugin: jest.fn(), +} as unknown as SlickGrid; + const mockAddon = jest.fn().mockImplementation(() => ({ init: jest.fn(), destroy: jest.fn() @@ -197,6 +204,71 @@ describe('extensionUtility', () => { expect(output).toBe('Commandes'); }); }); + + describe('readjustFrozenColumnIndexWhenNeeded method', () => { + let gridOptionsMock: GridOption; + + beforeEach(() => { + gridOptionsMock = { frozenColumn: 1 } as GridOption; + jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + jest.spyOn(SharedService.prototype, 'frozenVisibleColumnId', 'get').mockReturnValue('field2'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should increase "frozenColumn" from 0 to 1 when showing a column that was previously hidden and its index is lower or equal to provided argument (2nd arg, frozenColumnIndex)', () => { + const allColumns = [{ id: 'field1' }, { id: 'field2' }, { id: 'field3' }] as Column[]; + const visibleColumns = [{ id: 'field1' }, { id: 'field2' }] as Column[]; + const setOptionSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setOptions'); + + utility.readjustFrozenColumnIndexWhenNeeded('field1', 0, true, allColumns, visibleColumns); + + expect(setOptionSpy).toHaveBeenCalledWith({ frozenColumn: 1 }); + }); + + it('should keep "frozenColumn" at 0 when showing a column that was previously hidden and its index is greater than provided argument (2nd arg, frozenColumnIndex)', () => { + const allColumns = [{ id: 'field1' }, { id: 'field2' }, { id: 'field3' }] as Column[]; + const visibleColumns = [{ id: 'field1' }, { id: 'field2' }, { id: 'field3' }] as Column[]; + const setOptionSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setOptions'); + + utility.readjustFrozenColumnIndexWhenNeeded('field3', 0, true, allColumns, visibleColumns); + + expect(setOptionSpy).not.toHaveBeenCalled(); + }); + + it('should decrease "frozenColumn" from 1 to 0 when hiding a column that was previously shown and its index is lower or equal to provided argument (2nd arg, frozenColumnIndex)', () => { + const allColumns = [{ id: 'field1' }, { id: 'field2' }, { id: 'field3' }] as Column[]; + const visibleColumns = [{ id: 'field1' }, { id: 'field2' }] as Column[]; + const setOptionSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setOptions'); + + utility.readjustFrozenColumnIndexWhenNeeded('field1', 1, false, allColumns, visibleColumns); + + expect(setOptionSpy).toHaveBeenCalledWith({ frozenColumn: 0 }); + }); + + it('should keep "frozenColumn" at 1 when hiding a column that was previously hidden and its index is greater than provided argument (2nd arg, frozenColumnIndex)', () => { + const allColumns = [{ id: 'field1' }, { id: 'field2' }, { id: 'field3' }] as Column[]; + const visibleColumns = [{ id: 'field1' }, { id: 'field2' }] as Column[]; + const setOptionSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setOptions'); + + utility.readjustFrozenColumnIndexWhenNeeded('field3', 1, false, allColumns, visibleColumns); + + expect(setOptionSpy).not.toHaveBeenCalled(); + }); + + it('should not change "frozenColumn" when showing a column that was not found in the visibleColumns columns array', () => { + const allColumns = [{ id: 'field1' }, { id: 'field2' }, { id: 'field3' }] as Column[]; + const visibleColumns = [{ id: 'field1' }, { field: 'field2' }] as Column[]; + const setOptionSpy = jest.spyOn(SharedService.prototype.slickGrid, 'setOptions'); + + utility.readjustFrozenColumnIndexWhenNeeded('fiel3', 0, true, allColumns, visibleColumns); + + expect(setOptionSpy).not.toHaveBeenCalled(); + }); + }); }); describe('without Translate Service', () => { diff --git a/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts b/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts index c376e8d31..20ae33df9 100644 --- a/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts @@ -169,16 +169,18 @@ describe('gridMenuExtension', () => { const onCloseSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onMenuClose'); const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onCommand'); const visibleColsSpy = jest.spyOn(SharedService.prototype, 'visibleColumns', 'set'); + const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); const instance = extension.register() as SlickGridMenu; - instance.onColumnsChanged!.notify({ allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }, new Slick.EventData(), gridStub); + instance.onColumnsChanged!.notify({ columnId: 'field1', showing: false, allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }, new Slick.EventData(), gridStub); + expect(readjustSpy).not.toHaveBeenCalled(); expect(handlerSpy).toHaveBeenCalledTimes(5); expect(handlerSpy).toHaveBeenCalledWith( { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, expect.anything() ); - expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }); + expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { columnId: 'field1', showing: false, allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }); expect(onAfterSpy).not.toHaveBeenCalled(); expect(onBeforeSpy).not.toHaveBeenCalled(); expect(onCloseSpy).not.toHaveBeenCalled(); @@ -197,14 +199,14 @@ describe('gridMenuExtension', () => { const visibleColsSpy = jest.spyOn(SharedService.prototype, 'visibleColumns', 'set'); const instance = extension.register() as SlickGridMenu; - instance.onColumnsChanged!.notify({ allColumns: columnsMock, columns: columnsMock, grid: gridStub }, new Slick.EventData(), gridStub); + instance.onColumnsChanged!.notify({ columnId: 'field1', showing: true, allColumns: columnsMock, columns: columnsMock, grid: gridStub }, new Slick.EventData(), gridStub); expect(handlerSpy).toHaveBeenCalledTimes(5); expect(handlerSpy).toHaveBeenCalledWith( { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, expect.anything() ); - expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { allColumns: columnsMock, columns: columnsMock, grid: gridStub }); + expect(onColumnSpy).toHaveBeenCalledWith(expect.anything(), { columnId: 'field1', showing: true, allColumns: columnsMock, columns: columnsMock, grid: gridStub }); expect(onAfterSpy).not.toHaveBeenCalled(); expect(onBeforeSpy).not.toHaveBeenCalled(); expect(onCloseSpy).not.toHaveBeenCalled(); @@ -212,6 +214,22 @@ describe('gridMenuExtension', () => { expect(visibleColsSpy).toHaveBeenCalledWith(columnsMock); }); + it('should call internal "onColumnsChanged" event and expect "readjustFrozenColumnIndexWhenNeeded" method to be called when the grid is detected to be a frozen grid', () => { + gridOptionsMock.frozenColumn = 0; + const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); + const readjustSpy = jest.spyOn(extensionUtility, 'readjustFrozenColumnIndexWhenNeeded'); + + const instance = extension.register() as SlickGridMenu; + instance.onColumnsChanged.notify({ columnId: 'field1', showing: false, allColumns: columnsMock, columns: columnsMock.slice(0, 1), grid: gridStub }, new Slick.EventData(), gridStub); + + expect(handlerSpy).toHaveBeenCalledTimes(5); + expect(handlerSpy).toHaveBeenCalledWith( + { notify: expect.anything(), subscribe: expect.anything(), unsubscribe: expect.anything(), }, + expect.anything() + ); + expect(readjustSpy).toHaveBeenCalledWith('field1', 0, false, columnsMock, columnsMock.slice(0, 1)); + }); + it('should call internal event handler subscribe and expect the "onBeforeMenuShow" option to be called when addon notify is called', () => { const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe'); const onColumnSpy = jest.spyOn(SharedService.prototype.gridOptions.gridMenu as GridMenu, 'onColumnsChanged'); @@ -311,7 +329,7 @@ describe('gridMenuExtension', () => { const autoSizeSpy = jest.spyOn(gridStub, 'autosizeColumns'); const instance = extension.register() as SlickGridMenu; - instance!.onColumnsChanged!.notify({ grid: gridStub, allColumns: columnsMock, columns: columnsMock.slice(0, 1) }, new Slick.EventData(), gridStub); + instance!.onColumnsChanged!.notify({ columnId: 'field1', showing: true, grid: gridStub, allColumns: columnsMock, columns: columnsMock.slice(0, 1) }, new Slick.EventData(), gridStub); instance.onMenuClose!.notify({ allColumns: columnsMock, visibleColumns: columnsMock, grid: gridStub, menu: divElement }, new Slick.EventData(), gridStub); expect(handlerSpy).toHaveBeenCalled(); diff --git a/packages/common/src/extensions/__tests__/headerMenuExtension.spec.ts b/packages/common/src/extensions/__tests__/headerMenuExtension.spec.ts index 4a496ab6b..acd0a9be7 100644 --- a/packages/common/src/extensions/__tests__/headerMenuExtension.spec.ts +++ b/packages/common/src/extensions/__tests__/headerMenuExtension.spec.ts @@ -316,6 +316,7 @@ describe('headerMenuExtension', () => { jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(1); jest.spyOn(gridStub, 'getColumns').mockReturnValue(columnsMock); const setColumnsSpy = jest.spyOn(gridStub, 'setColumns'); + const setOptionSpy = jest.spyOn(gridStub, 'setOptions'); const visibleSpy = jest.spyOn(SharedService.prototype, 'visibleColumns', 'set'); const updatedColumnsMock = [{ id: 'field1', field: 'field1', nameKey: 'TITLE', width: 100, @@ -332,6 +333,35 @@ describe('headerMenuExtension', () => { extension.hideColumn(columnsMock[1]); + expect(setOptionSpy).not.toHaveBeenCalled(); + expect(visibleSpy).toHaveBeenCalledWith(updatedColumnsMock); + expect(setColumnsSpy).toHaveBeenCalledWith(updatedColumnsMock); + }); + + it('should call hideColumn and expect "setOptions" to be called with new "frozenColumn" index when the grid is detected to be a frozen grid', () => { + gridOptionsMock.frozenColumn = 1; + jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); + jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(1); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(columnsMock); + const setColumnsSpy = jest.spyOn(gridStub, 'setColumns'); + const setOptionSpy = jest.spyOn(gridStub, 'setOptions'); + const visibleSpy = jest.spyOn(SharedService.prototype, 'visibleColumns', 'set'); + const updatedColumnsMock = [{ + id: 'field1', field: 'field1', nameKey: 'TITLE', width: 100, + header: { + menu: { + items: [ + { iconCssClass: 'fa fa-thumb-tack', title: 'Geler les colonnes', command: 'freeze-columns', positionOrder: 48 }, + { divider: true, command: '', positionOrder: 49 }, + { command: 'hide', iconCssClass: 'fa fa-times', positionOrder: 55, title: 'Cacher la colonne' } + ] + } + } + }] as Column[]; + + extension.hideColumn(columnsMock[1]); + + expect(setOptionSpy).toHaveBeenCalledWith({ frozenColumn: 0 }); expect(visibleSpy).toHaveBeenCalledWith(updatedColumnsMock); expect(setColumnsSpy).toHaveBeenCalledWith(updatedColumnsMock); }); diff --git a/packages/common/src/extensions/columnPickerExtension.ts b/packages/common/src/extensions/columnPickerExtension.ts index ce5243fff..7b3fef26a 100644 --- a/packages/common/src/extensions/columnPickerExtension.ts +++ b/packages/common/src/extensions/columnPickerExtension.ts @@ -64,6 +64,13 @@ export class ColumnPickerExtension implements Extension { if (args && Array.isArray(args.columns) && args.columns.length !== this.sharedService.visibleColumns.length) { this.sharedService.visibleColumns = args.columns; } + // if we're using frozen columns, we need to readjust pinning when the new hidden column becomes visible again on the left pinning container + // we need to readjust frozenColumn index because SlickGrid freezes by index and has no knowledge of the columns themselves + const frozenColumnIndex = this.sharedService.gridOptions.frozenColumn ?? -1; + if (frozenColumnIndex >= 0) { + const { showing: isColumnShown, columnId, allColumns, columns: visibleColumns } = args; + this.extensionUtility.readjustFrozenColumnIndexWhenNeeded(columnId, frozenColumnIndex, isColumnShown, allColumns, visibleColumns); + } }); } return this._addon; diff --git a/packages/common/src/extensions/extensionUtility.ts b/packages/common/src/extensions/extensionUtility.ts index f6a6b0af0..5f3322e6c 100644 --- a/packages/common/src/extensions/extensionUtility.ts +++ b/packages/common/src/extensions/extensionUtility.ts @@ -1,6 +1,7 @@ import { Constants } from '../constants'; import { ExtensionName } from '../enums/extensionName.enum'; +import { Column } from '../interfaces/column.interface'; import { SharedService } from '../services/shared.service'; import { TranslaterService } from '../services'; import { getTranslationPrefix } from '../services/utilities'; @@ -125,6 +126,42 @@ export class ExtensionUtility { } } + /** + * When using ColumnPicker/GridMenu to show/hide a column, we potentially need to readjust the grid option "frozenColumn" index. + * That is because SlickGrid freezes by column index and it has no knowledge of the columns themselves and won't change the index, we need to do that ourselves whenever necessary. + * Note: we call this method right after the visibleColumns array got updated, it won't work properly if we call it before the setting the visibleColumns. + * @param {String} pickerColumnId - what is the column id triggered by the picker + * @param {Number} frozenColumnIndex - current frozenColumn index + * @param {Boolean} showingColumn - is the column being shown or hidden? + * @param {Array} allColumns - all columns (including hidden ones) + * @param {Array} visibleColumns - only visible columns (excluding hidden ones) + */ + readjustFrozenColumnIndexWhenNeeded(pickerColumnId: string | number, frozenColumnIndex: number, showingColumn: boolean, allColumns: Column[], visibleColumns: Column[]) { + if (frozenColumnIndex >= 0 && pickerColumnId) { + // calculate a possible frozenColumn index variance + let frozenColIndexVariance = 0; + if (showingColumn) { + const definedFrozenColumnIndex = visibleColumns.findIndex(col => col.id === this.sharedService.frozenVisibleColumnId); + const columnIndex = visibleColumns.findIndex(col => col.id === pickerColumnId); + frozenColIndexVariance = (columnIndex >= 0 && (frozenColumnIndex >= columnIndex || definedFrozenColumnIndex === columnIndex)) ? 1 : 0; + } else { + const columnIndex = allColumns.findIndex(col => col.id === pickerColumnId); + frozenColIndexVariance = (columnIndex >= 0 && frozenColumnIndex >= columnIndex) ? -1 : 0; + } + // if we have a variance different than 0 then apply it + const newFrozenColIndex = frozenColumnIndex + frozenColIndexVariance; + if (frozenColIndexVariance !== 0) { + this.sharedService.slickGrid.setOptions({ frozenColumn: newFrozenColIndex }); + } + + // to freeze columns, we need to take only the visible columns and we also need to use setColumns() when some of them are hidden + // to make sure that we only use the visible columns, not doing this would show back some of the hidden columns + if (Array.isArray(visibleColumns) && Array.isArray(allColumns) && visibleColumns.length !== allColumns.length) { + this.sharedService.slickGrid.setColumns(visibleColumns); + } + } + } + /** * Sort items (by pointers) in an array by a property name * @params items array diff --git a/packages/common/src/extensions/gridMenuExtension.ts b/packages/common/src/extensions/gridMenuExtension.ts index 9465b4004..04ed7343b 100644 --- a/packages/common/src/extensions/gridMenuExtension.ts +++ b/packages/common/src/extensions/gridMenuExtension.ts @@ -129,6 +129,13 @@ export class GridMenuExtension implements Extension { if (args && Array.isArray(args.columns) && args.columns.length > this.sharedService.visibleColumns.length) { this.sharedService.visibleColumns = args.columns; } + // if we're using frozen columns, we need to readjust pinning when the new hidden column becomes visible again on the left pinning container + // we need to readjust frozenColumn index because SlickGrid freezes by index and has no knowledge of the columns themselves + const frozenColumnIndex = this.sharedService.gridOptions.frozenColumn ?? -1; + if (frozenColumnIndex >= 0) { + const { showing: isColumnShown, columnId, allColumns, columns: visibleColumns } = args; + this.extensionUtility.readjustFrozenColumnIndexWhenNeeded(columnId, frozenColumnIndex, isColumnShown, allColumns, visibleColumns); + } }); } diff --git a/packages/common/src/extensions/headerMenuExtension.ts b/packages/common/src/extensions/headerMenuExtension.ts index 0a17a973e..8f4bc7e8e 100644 --- a/packages/common/src/extensions/headerMenuExtension.ts +++ b/packages/common/src/extensions/headerMenuExtension.ts @@ -233,6 +233,15 @@ export class HeaderMenuExtension implements Extension { if (this.sharedService.slickGrid && this.sharedService.slickGrid.getColumns && this.sharedService.slickGrid.setColumns && this.sharedService.slickGrid.getColumnIndex) { const columnIndex = this.sharedService.slickGrid.getColumnIndex(column.id); const currentColumns = this.sharedService.slickGrid.getColumns(); + + // if we're using frozen columns, we need to readjust pinning when the new hidden column is on the left pinning container + // we need to do this because SlickGrid freezes by index and has no knowledge of the columns themselves + const frozenColumnIndex = this.sharedService.gridOptions.frozenColumn || -1; + if (frozenColumnIndex >= 0 && frozenColumnIndex >= columnIndex) { + this.sharedService.slickGrid.setOptions({ frozenColumn: frozenColumnIndex - 1 }); + } + + // then proceed with hiding the column in SlickGrid & trigger an event when done const visibleColumns = arrayRemoveItemByIndex(currentColumns, columnIndex); this.sharedService.visibleColumns = visibleColumns; this.sharedService.slickGrid.setColumns(visibleColumns); @@ -344,6 +353,7 @@ export class HeaderMenuExtension implements Extension { const visibleColumns = [...this.sharedService.visibleColumns]; const columnPosition = visibleColumns.findIndex((col) => col.id === args.column.id); this.sharedService.slickGrid.setOptions({ frozenColumn: columnPosition }); + this.sharedService.frozenVisibleColumnId = args.column.id; // to freeze columns, we need to take only the visible columns and we also need to use setColumns() when some of them are hidden // to make sure that we only use the visible columns, not doing this would show back some of the hidden columns diff --git a/packages/common/src/interfaces/slickColumnPicker.interface.ts b/packages/common/src/interfaces/slickColumnPicker.interface.ts index e0de66da6..5332bde24 100644 --- a/packages/common/src/interfaces/slickColumnPicker.interface.ts +++ b/packages/common/src/interfaces/slickColumnPicker.interface.ts @@ -29,5 +29,20 @@ export interface SlickColumnPicker { // Events /** SlickGrid Event fired when any of the columns checkbox selection changes. */ - onColumnsChanged: SlickEvent<{ allColumns: Column[], columns: Column[], grid: SlickGrid }>; + onColumnsChanged: SlickEvent<{ + /** columnId that triggered the picker column change */ + columnId: string, + + /** is the column showing or hiding? */ + showing: boolean, + + /** all columns (including hidden ones) */ + allColumns: Column[], + + /** only visible columns (excluding hidden columns) */ + columns: Column[], + + /** Slick Grid object */ + grid: SlickGrid; + }>; } diff --git a/packages/common/src/interfaces/slickGridMenu.interface.ts b/packages/common/src/interfaces/slickGridMenu.interface.ts index 378bbb30c..e8faf78dd 100644 --- a/packages/common/src/interfaces/slickGridMenu.interface.ts +++ b/packages/common/src/interfaces/slickGridMenu.interface.ts @@ -48,16 +48,61 @@ export interface SlickGridMenu { // Events /** SlickGrid Event fired After the menu is shown. */ - onAfterMenuShow?: SlickEvent<{ grid: SlickGrid; menu: HTMLElement; columns: Column[] }>; + onAfterMenuShow?: SlickEvent<{ + /** Slick Grid object */ + grid: SlickGrid; + + /** Grid Menu DOM element */ + menu: HTMLElement; + + /** only visible columns (excluding hidden columns) */ + columns: Column[] + }>; /** SlickGrid Event fired Before the menu is shown. */ - onBeforeMenuShow?: SlickEvent<{ grid: SlickGrid; menu: HTMLElement; columns: Column[] }>; + onBeforeMenuShow?: SlickEvent<{ + /** Slick Grid object */ + grid: SlickGrid; + + /** Grid Menu DOM element */ + menu: HTMLElement; + + /** only visible columns (excluding hidden columns) */ + columns: Column[] + }>; /** SlickGrid Event fired when any of the columns checkbox selection changes. */ - onColumnsChanged?: SlickEvent<{ grid: SlickGrid; allColumns: Column[]; columns: Column[]; }>; + onColumnsChanged?: SlickEvent<{ + /** columnId that triggered the picker column change */ + columnId: string, + + /** is the column showing or hiding? */ + showing: boolean, + + /** all columns (including hidden ones) */ + allColumns: Column[], + + /** only visible columns (excluding hidden columns) */ + columns: Column[], + + /** Slick Grid object */ + grid: SlickGrid; + }>; /** SlickGrid Event fired when the menu is closing. */ - onMenuClose?: SlickEvent<{ grid: SlickGrid; menu: HTMLElement; allColumns: Column[], visibleColumns: Column[] }>; + onMenuClose?: SlickEvent<{ + /** Slick Grid object */ + grid: SlickGrid; + + /** Grid Menu DOM element */ + menu: HTMLElement; + + /** all columns (including hidden ones) */ + allColumns: Column[]; + + /** only visible columns (excluding hidden columns) */ + visibleColumns: Column[] + }>; /** SlickGrid Event fired on menu option clicked from the Command items list */ onCommand?: SlickEvent; diff --git a/packages/common/src/services/__tests__/shared.service.spec.ts b/packages/common/src/services/__tests__/shared.service.spec.ts index 07ff648bb..bc41dad97 100644 --- a/packages/common/src/services/__tests__/shared.service.spec.ts +++ b/packages/common/src/services/__tests__/shared.service.spec.ts @@ -206,6 +206,16 @@ describe('Shared Service', () => { expect(output).toEqual(mockColumns); }); + it('should call "frozenVisibleColumnId" GETTER and expect a boolean value to be returned', () => { + const columnId = service.frozenVisibleColumnId; + expect(columnId).toEqual(undefined); + }); + + it('should call "frozenVisibleColumnId" GETTER and SETTER expect same value to be returned', () => { + service.frozenVisibleColumnId = 'field1'; + expect(service.frozenVisibleColumnId).toEqual('field1'); + }); + it('should call "visibleColumns" GETTER and return all columns', () => { const spy = jest.spyOn(service, 'visibleColumns', 'get').mockReturnValue(mockColumns); diff --git a/packages/common/src/services/shared.service.ts b/packages/common/src/services/shared.service.ts index 75cb5baaa..930e342b6 100644 --- a/packages/common/src/services/shared.service.ts +++ b/packages/common/src/services/shared.service.ts @@ -13,6 +13,7 @@ export class SharedService { private _hierarchicalDataset: any[] | undefined; private _internalPubSubService: PubSubService; private _externalRegisteredServices: any[]; + private _frozenVisibleColumnId: string | number; // -- // public @@ -50,6 +51,15 @@ export class SharedService { this._dataView = dataView; } + /** Setter to keep the frozen column id for reference if we ever show/hide column from ColumnPicker/GridMenu afterward */ + get frozenVisibleColumnId(): string | number { + return this._frozenVisibleColumnId; + } + /** Getter to keep the frozen column id for reference if we ever show/hide column from ColumnPicker/GridMenu afterward */ + set frozenVisibleColumnId(columnId: string | number) { + this._frozenVisibleColumnId = columnId; + } + /** Getter for SlickGrid Grid object */ get slickGrid(): SlickGrid { return this._grid; 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 602db5a11..05b6f87d4 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 @@ -342,6 +342,14 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () expect(loadSpy).toHaveBeenCalled(); }); + it('should keep frozen column index reference (via frozenVisibleColumnId) when grid is a frozen grid', () => { + const sharedFrozenIndexSpy = jest.spyOn(SharedService.prototype, 'frozenVisibleColumnId', 'set'); + component.gridOptions.frozenColumn = 0; + component.initialization(divContainer, slickEventHandler); + + expect(sharedFrozenIndexSpy).toHaveBeenCalledWith('name'); + }); + it('should create a grid and expect multiple Event Aggregator being called', () => { const pubSubSpy = jest.spyOn(eventPubSubService, 'publish'); 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 115efe383..42c1b611a 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -476,6 +476,12 @@ export class SlickVanillaGridBundle { this.bindDifferentHooks(this.slickGrid, this._gridOptions, this.dataView); this._slickgridInitialized = true; + // when it's a frozen grid, we need to keep the frozen column id for reference if we ever show/hide column from ColumnPicker/GridMenu afterward + const frozenColumnIndex = this._gridOptions?.frozenColumn ?? -1; + if (frozenColumnIndex >= 0 && frozenColumnIndex <= this._columnDefinitions.length) { + this.sharedService.frozenVisibleColumnId = this._columnDefinitions[frozenColumnIndex]?.id ?? ''; + } + // initialize the SlickGrid grid this.slickGrid.init(); diff --git a/test/cypress.json b/test/cypress.json index 0487e6b1a..42ab0739c 100644 --- a/test/cypress.json +++ b/test/cypress.json @@ -1,7 +1,6 @@ { "baseUrl": "http://localhost:8888", "baseExampleUrl": "http://localhost:8888/#", - "runMode": 2, "video": false, "viewportWidth": 1000, "viewportHeight": 950, @@ -13,5 +12,9 @@ "supportFile": "test/cypress/support/index.js", "videosFolder": "test/cypress/videos", "defaultCommandTimeout": 5000, - "pageLoadTimeout": 90000 + "pageLoadTimeout": 90000, + "retries": { + "runMode": 2, + "openMode": 0 + } } diff --git a/test/cypress/integration/example04.spec.js b/test/cypress/integration/example04.spec.js index fc5cbb30e..a298591c3 100644 --- a/test/cypress/integration/example04.spec.js +++ b/test/cypress/integration/example04.spec.js @@ -36,6 +36,128 @@ describe('Example 04 - Frozen Grid', () => { cy.get('.grid-canvas-right > [style="top:0px"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); }); + it('should hide "Title" column from Grid Menu and expect last frozen column to be "% Complete"', () => { + const newColumnList = ['', '% Complete', 'Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + + cy.get('.grid4') + .find('button.slick-gridmenu-button') + .click({ force: true }); + + cy.get('.grid4') + .get('.slick-gridmenu:visible') + .find('.slick-gridmenu-list') + .children('li:visible:nth(0)') + .children('label') + .should('contain', 'Title') + .click({ force: true }); + + cy.get('.grid4') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + + cy.get('.grid-canvas-left > [style="top:0px"]').children().should('have.length', 2 * 2); + cy.get('.grid-canvas-right > [style="top:0px"]').children().should('have.length', 6 * 2); + + cy.get('.grid-canvas-left > [style="top:0px"] > .slick-cell:nth(0)').should('contain', ''); + + cy.get('.grid-canvas-right > [style="top:0px"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-right > [style="top:0px"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + it('should show again "Title" column from Grid Menu and expect last frozen column to still be "% Complete"', () => { + cy.get('.grid4') + .get('.slick-gridmenu:visible') + .find('.slick-gridmenu-list') + .children('li:visible:nth(0)') + .children('label') + .should('contain', 'Title') + .click({ force: true }); + + cy.get('.grid4') + .get('.slick-gridmenu:visible') + .find('span.close') + .click({ force: true }); + + cy.get('.grid4') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + + cy.get('.grid-canvas-left > [style="top:0px"]').children().should('have.length', 3 * 2); + cy.get('.grid-canvas-right > [style="top:0px"]').children().should('have.length', 6 * 2); + + cy.get('.grid-canvas-left > [style="top:0px"] > .slick-cell:nth(0)').should('contain', ''); + cy.get('.grid-canvas-left > [style="top:0px"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + + cy.get('.grid-canvas-right > [style="top:0px"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-right > [style="top:0px"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + it('should hide "Title" column from Header Menu and expect last frozen column to be "% Complete"', () => { + const newColumnList = ['', '% Complete', 'Start', 'Finish', 'Completed', 'Cost | Duration', 'City of Origin', 'Action']; + + cy.get('.grid4') + .find('.slick-header-column:nth(1)') + .trigger('mouseover') + .children('.slick-header-menubutton') + .click(); + + cy.get('.slick-header-menu') + .should('be.visible') + .children('.slick-header-menuitem:nth-child(8)') + .children('.slick-header-menucontent') + .should('contain', 'Hide Column') + .click(); + + cy.get('.grid4') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + + cy.get('.grid-canvas-left > [style="top:0px"]').children().should('have.length', 2 * 2); + cy.get('.grid-canvas-right > [style="top:0px"]').children().should('have.length', 6 * 2); + + cy.get('.grid-canvas-left > [style="top:0px"] > .slick-cell:nth(0)').should('contain', ''); + + cy.get('.grid-canvas-right > [style="top:0px"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-right > [style="top:0px"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + it('should show again "Title" column from Column Picker and expect last frozen column to still be "% Complete"', () => { + cy.get('.grid4') + .find('.slick-header-column:nth(4)') + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-columnpicker') + .find('.slick-columnpicker-list') + .children('li:nth-child(2)') + .children('label') + .should('contain', 'Title') + .click(); + + cy.get('.slick-columnpicker:visible') + .find('span.close') + .trigger('click') + .click(); + + cy.get('.grid4') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + + cy.get('.grid-canvas-left > [style="top:0px"]').children().should('have.length', 3 * 2); + cy.get('.grid-canvas-right > [style="top:0px"]').children().should('have.length', 6 * 2); + + cy.get('.grid-canvas-left > [style="top:0px"] > .slick-cell:nth(0)').should('contain', ''); + cy.get('.grid-canvas-left > [style="top:0px"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + + cy.get('.grid-canvas-right > [style="top:0px"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-right > [style="top:0px"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + it('should click on the "Remove Frozen Columns" button to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { cy.get('[data-test=remove-frozen-column-button]') .click({ force: true }); diff --git a/yarn.lock b/yarn.lock index 7549b33f1..97d179cf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10819,10 +10819,10 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" -slickgrid@^2.4.31: - version "2.4.31" - resolved "https://registry.yarnpkg.com/slickgrid/-/slickgrid-2.4.31.tgz#becbcdd6b8970455ff909a7651dfb2cf2cdcffde" - integrity sha512-2f+YuyOTo81oDPnRbz/XlvSNVkDvJoS4lm/16njnQV2q8SYHHUO3MojgXHvpTqyxfKI1P1r7qQoGItrupo56Zg== +slickgrid@^2.4.32: + version "2.4.32" + resolved "https://registry.yarnpkg.com/slickgrid/-/slickgrid-2.4.32.tgz#4a71c06629bcc82a63261abe98da9027cb5ddc6c" + integrity sha512-tejeg6urqkBPMdCgR2a/8UuLz6Dc3K7KTUsmGgX+QWZlCIpGni+YdOxANAZsnq/eHTZt8cTDoEZQAD9InVLxyw== dependencies: jquery ">=1.8.0" jquery-ui ">=1.8.0"