Skip to content

Commit

Permalink
feat(filters): add updateSingleFilter for a single external filter (#265
Browse files Browse the repository at this point in the history
)
  • Loading branch information
ghiscoding committed Feb 16, 2021
1 parent c234013 commit 20564a3
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 23 deletions.
25 changes: 5 additions & 20 deletions examples/webpack-demo-vanilla-bundle/src/examples/example08.ts
Expand Up @@ -2,7 +2,6 @@ import {
Column,
GridOption,
FieldType,
FilterCallbackArg,
OperatorString,
} from '@slickgrid-universal/common';
import { ExcelExportService } from '@slickgrid-universal/excel-export';
Expand Down Expand Up @@ -103,6 +102,7 @@ export class Example08 {
explicitInitialization: true,
frozenColumn: 2,
rowHeight: 33,
showCustomFooter: true,
gridMenu: { hideClearFrozenColumnsCommand: false },
headerMenu: { hideFreezeColumnsCommand: false },

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
}
}
90 changes: 90 additions & 0 deletions packages/common/src/services/__tests__/filter.service.spec.ts
Expand Up @@ -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;
Expand Down
Expand Up @@ -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', () => {
Expand Down
49 changes: 49 additions & 0 deletions packages/common/src/services/filter.service.ts
Expand Up @@ -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
// -------------------
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/services/groupingAndColspan.service.ts
Expand Up @@ -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<SlickColumnPicker>(ExtensionName.columnPicker);
Expand Down
20 changes: 20 additions & 0 deletions test/cypress/integration/example08.spec.js
@@ -1,5 +1,9 @@
/// <reference types="cypress" />

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'];
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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`);
});
});
});

0 comments on commit 20564a3

Please sign in to comment.