diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example08.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example08.ts index f3414f03f..852c70983 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example08.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example08.ts @@ -2,7 +2,6 @@ import { Column, GridOption, FieldType, - FilterCallbackArg, OperatorString, } from '@slickgrid-universal/common'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; @@ -103,6 +102,7 @@ export class Example08 { explicitInitialization: true, frozenColumn: 2, rowHeight: 33, + showCustomFooter: true, gridMenu: { hideClearFrozenColumnsCommand: false }, headerMenu: { hideFreezeColumnsCommand: false }, @@ -184,7 +184,7 @@ export class Example08 { selectOption.label = columnDef.name; columnSelect.appendChild(selectOption); } - this.grid2SearchSelectedColumn = this.columnDefinitions2[0]; + this.grid2SearchSelectedColumn = this.columnDefinitions2.find(col => col.id === 'title'); } populategrid2SearchOperatorDropdown() { @@ -215,25 +215,10 @@ export class Example08 { } updateFilter() { - const columnId = this.grid2SearchSelectedColumn?.id; - const filter = {}; - const filterArg: FilterCallbackArg = { - columnDef: this.grid2SearchSelectedColumn, - operator: this.grid2SelectedOperator as OperatorString, // or fix one yourself like '=' + this.sgb2.filterService.updateSingleFilter({ + columnId: `${this.grid2SearchSelectedColumn?.id ?? ''}`, + operator: this.grid2SelectedOperator as OperatorString, searchTerms: [this.grid2SearchValue || ''] - }; - if (this.grid2SearchValue) { - // pass a columnFilter object as an object which it's property name must be a column field name (e.g.: 'duration': {...} ) - filter[columnId] = filterArg; - } - - // const currentFilter = { columnId, operator: this.grid2SelectedOperator as OperatorString, searchTerms: this.grid2SearchValue || '' } as CurrentFilter; - // this.sgb2.filterService.updateSingleFilter(currentFilter); - this.sgb2.dataView.setFilterArgs({ - columnFilters: filter, - grid: this.sgb2.slickGrid }); - this.sgb2.dataView.refresh(); - this.sgb2.slickGrid.invalidate(); } } diff --git a/packages/common/src/services/__tests__/filter.service.spec.ts b/packages/common/src/services/__tests__/filter.service.spec.ts index 472c357a1..1cf0cd3f1 100644 --- a/packages/common/src/services/__tests__/filter.service.spec.ts +++ b/packages/common/src/services/__tests__/filter.service.spec.ts @@ -1159,6 +1159,96 @@ describe('FilterService', () => { }); }); + describe('updateSingleFilter method', () => { + let mockColumn1: Column; + let mockColumn2: Column; + let mockArgs1; + let mockArgs2; + + beforeEach(() => { + gridOptionMock.enableFiltering = true; + gridOptionMock.backendServiceApi = undefined; + mockColumn1 = { id: 'firstName', name: 'firstName', field: 'firstName', }; + mockColumn2 = { id: 'isActive', name: 'isActive', field: 'isActive', type: FieldType.boolean, }; + mockArgs1 = { grid: gridStub, column: mockColumn1, node: document.getElementById(DOM_ELEMENT_ID) }; + mockArgs2 = { grid: gridStub, column: mockColumn2, node: document.getElementById(DOM_ELEMENT_ID) }; + sharedService.allColumns = [mockColumn1, mockColumn2]; + }); + + it('should call "updateSingleFilter" method and expect event "emitFilterChanged" to be trigged local when using "bindLocalOnFilter" and also expect filters to be set in dataview', () => { + const expectation = { + firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string }, + }; + const emitSpy = jest.spyOn(service, 'emitFilterChanged'); + const setFilterArgsSpy = jest.spyOn(dataViewStub, 'setFilterArgs'); + const refreshSpy = jest.spyOn(dataViewStub, 'refresh'); + service.init(gridStub); + service.bindLocalOnFilter(gridStub); + gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub); + gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub); + service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' }); + + expect(setFilterArgsSpy).toHaveBeenCalledWith({ columnFilters: expectation, grid: gridStub }); + expect(refreshSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith('local'); + expect(service.getColumnFilters()).toEqual({ + firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string }, + }); + }); + + it('should call "updateSingleFilter" method and expect event "emitFilterChanged" to be trigged local when using "bindBackendOnFilter" and also expect filters to be set in dataview', () => { + const expectation = { + firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string }, + }; + gridOptionMock.backendServiceApi = { + filterTypingDebounce: 0, + service: backendServiceStub, + process: () => new Promise((resolve) => resolve(jest.fn())), + }; + const emitSpy = jest.spyOn(service, 'emitFilterChanged'); + const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters'); + const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged'); + + service.init(gridStub); + service.bindBackendOnFilter(gridStub); + gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub); + gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub); + service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' }); + + expect(emitSpy).toHaveBeenCalledWith('remote'); + expect(backendProcessSpy).not.toHaveBeenCalled(); + expect(backendUpdateSpy).toHaveBeenCalledWith(expectation, true); + expect(service.getColumnFilters()).toEqual(expectation); + expect(mockRefreshBackendDataset).toHaveBeenCalledWith(gridOptionMock); + }); + + it('should expect filter to be sent to the backend when using "bindBackendOnFilter" without triggering a filter changed event neither a backend query when both flag arguments are set to false', () => { + const expectation = { + firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string }, + }; + gridOptionMock.backendServiceApi = { + filterTypingDebounce: 0, + service: backendServiceStub, + process: () => new Promise((resolve) => resolve(jest.fn())), + }; + const emitSpy = jest.spyOn(service, 'emitFilterChanged'); + const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters'); + const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged'); + + service.init(gridStub); + service.bindBackendOnFilter(gridStub); + gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub); + gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub); + service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' }, false, false); + + expect(backendProcessSpy).not.toHaveBeenCalled(); + expect(emitSpy).not.toHaveBeenCalled(); + expect(mockRefreshBackendDataset).not.toHaveBeenCalled(); + expect(backendUpdateSpy).toHaveBeenCalledWith(expectation, true); + expect(service.getColumnFilters()).toEqual(expectation); + }); + }); + describe('disableFilterFunctionality method', () => { beforeEach(() => { gridOptionMock.enableFiltering = true; diff --git a/packages/common/src/services/__tests__/groupingAndColspan.service.spec.ts b/packages/common/src/services/__tests__/groupingAndColspan.service.spec.ts index 91dee7676..a8caacf3b 100644 --- a/packages/common/src/services/__tests__/groupingAndColspan.service.spec.ts +++ b/packages/common/src/services/__tests__/groupingAndColspan.service.spec.ts @@ -200,8 +200,9 @@ describe('GroupingAndColspanService', () => { jest.runAllTimers(); // fast-forward timer expect(spy).toHaveBeenCalledTimes(2); - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 75); + expect(setTimeout).toHaveBeenCalledTimes(2); + expect(setTimeout).toHaveBeenNthCalledWith(1, expect.any(Function), 75); + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 0); }); it('should call the "renderPreHeaderRowGroupingTitles" after triggering a grid resize', () => { diff --git a/packages/common/src/services/filter.service.ts b/packages/common/src/services/filter.service.ts index 088051cf3..e30c6bc4f 100644 --- a/packages/common/src/services/filter.service.ts +++ b/packages/common/src/services/filter.service.ts @@ -813,6 +813,55 @@ export class FilterService { } } + /** + * Update a Single Filter dynamically just by providing (columnId, operator and searchTerms) + * You can also choose emit (default) a Filter Changed event that will be picked by the Grid State Service. + * + * Also for backend service only, you can choose to trigger a backend query (default) or not if you wish to do it later, + * this could be useful when using updateFilters & updateSorting and you wish to only send the backend query once. + * @param filters array + * @param triggerEvent defaults to True, do we want to emit a filter changed event? + */ + updateSingleFilter(filter: CurrentFilter, emitChangedEvent = true, triggerBackendQuery = true) { + const columnDef = this.sharedService.allColumns.find(col => col.id === filter.columnId); + if (columnDef && filter.columnId) { + this._columnFilters = {}; + if (Array.isArray(filter.searchTerms) && (filter.searchTerms.length > 1 || (filter.searchTerms.length === 1 && filter.searchTerms[0] !== ''))) { + // pass a columnFilter object as an object which it's property name must be a column field name (e.g.: 'duration': {...} ) + this._columnFilters[filter.columnId] = { + columnId: filter.columnId, + operator: filter.operator, + searchTerms: filter.searchTerms, + columnDef, + type: columnDef.type ?? FieldType.string, + }; + } + + const backendApi = this._gridOptions && this._gridOptions.backendServiceApi; + + if (backendApi) { + const backendApiService = backendApi && backendApi.service; + if (backendApiService && backendApiService.updateFilters) { + backendApiService.updateFilters(this._columnFilters, true); + if (triggerBackendQuery) { + refreshBackendDataset(this._gridOptions); + } + } + } else { + this._dataView.setFilterArgs({ + columnFilters: this._columnFilters, + grid: this._grid + }); + this._dataView.refresh(); + } + + if (emitChangedEvent) { + const emitterType = backendApi ? EmitterType.remote : EmitterType.local; + this.emitFilterChanged(emitterType); + } + } + } + // -- // protected functions // ------------------- diff --git a/packages/common/src/services/groupingAndColspan.service.ts b/packages/common/src/services/groupingAndColspan.service.ts index d32fba87d..74a5eabcc 100644 --- a/packages/common/src/services/groupingAndColspan.service.ts +++ b/packages/common/src/services/groupingAndColspan.service.ts @@ -65,7 +65,7 @@ export class GroupingAndColspanService { this._eventHandler.subscribe(grid.onSort, () => this.renderPreHeaderRowGroupingTitles()); this._eventHandler.subscribe(grid.onColumnsResized, () => this.renderPreHeaderRowGroupingTitles()); this._eventHandler.subscribe(grid.onColumnsReordered, () => this.renderPreHeaderRowGroupingTitles()); - this._eventHandler.subscribe(this._dataView.onRowCountChanged, () => this.renderPreHeaderRowGroupingTitles()); + this._eventHandler.subscribe(this._dataView.onRowCountChanged, () => this.delayRenderPreHeaderRowGroupingTitles(0)); // for both picker (columnPicker/gridMenu) we also need to re-create after hiding/showing columns const columnPickerExtension = this.extensionService.getExtensionByName(ExtensionName.columnPicker); diff --git a/test/cypress/integration/example08.spec.js b/test/cypress/integration/example08.spec.js index 944742656..7520482d0 100644 --- a/test/cypress/integration/example08.spec.js +++ b/test/cypress/integration/example08.spec.js @@ -1,5 +1,9 @@ /// +function removeExtraSpaces(textS) { + return `${textS}`.replace(/\s+/g, ' ').trim(); +} + describe('Example 08 - Column Span & Header Grouping', () => { // NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after clear frozen columns const fullPreTitles = ['', 'Common Factor', 'Period', 'Analysis']; @@ -135,6 +139,14 @@ describe('Example 08 - Column Span & Header Grouping', () => { cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('contain', 'Task 25'); cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(1)`).should('contain', 'Task 35'); cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 4}px"] > .slick-cell:nth(1)`).should('contain', 'Task 45'); + + cy.get('.grid2') + .find('.slick-custom-footer') + .find('.right-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + expect(text).to.eq(`50 of 500 items`); + }); }); it('should search for "% Complete" below 50 and expect rows to be that', () => { @@ -180,5 +192,13 @@ describe('Example 08 - Column Span & Header Grouping', () => { cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('contain', 'Task 2'); cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(1)`).should('contain', 'Task 3'); cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 4}px"] > .slick-cell:nth(1)`).should('contain', 'Task 4'); + + cy.get('.grid2') + .find('.slick-custom-footer') + .find('.right-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + expect(text).to.eq(`500 of 500 items`); + }); }); });