From a25791a5d11b73fd88d80ef8a6f788b27d7390ec Mon Sep 17 00:00:00 2001 From: Ghislain B Date: Sat, 12 Nov 2022 18:55:00 -0500 Subject: [PATCH] feat(common): add "targetSelector" to onFilterChanged & Grid State (#813) --- .../src/interfaces/columnFilter.interface.ts | 3 + .../src/interfaces/currentFilter.interface.ts | 3 + .../interfaces/filterChangedArgs.interface.ts | 1 + .../searchColumnFilter.interface.ts | 3 + .../services/__tests__/domUtilities.spec.ts | 32 +++++++ .../services/__tests__/filter.service.spec.ts | 85 ++++++++++--------- packages/common/src/services/domUtilities.ts | 8 ++ .../common/src/services/filter.service.ts | 8 +- .../__tests__/graphql.service.spec.ts | 11 +-- .../graphql/src/services/graphql.service.ts | 3 + .../__tests__/grid-odata.service.spec.ts | 19 +++-- .../odata/src/services/grid-odata.service.ts | 3 + test/cypress/e2e/example09.cy.js | 2 +- test/cypress/e2e/example15.cy.js | 2 +- 14 files changed, 127 insertions(+), 56 deletions(-) diff --git a/packages/common/src/interfaces/columnFilter.interface.ts b/packages/common/src/interfaces/columnFilter.interface.ts index b54677dff..539024ec5 100644 --- a/packages/common/src/interfaces/columnFilter.interface.ts +++ b/packages/common/src/interfaces/columnFilter.interface.ts @@ -129,6 +129,9 @@ export interface ColumnFilter { */ skipCompoundOperatorFilterWithNullInput?: boolean; + /** Target element selector from which the filter was triggered from. */ + targetSelector?: string; + /** What is the Field Type that can be used by the Filter (as precedence over the "type" set the column definition) */ type?: typeof FieldType[keyof typeof FieldType]; diff --git a/packages/common/src/interfaces/currentFilter.interface.ts b/packages/common/src/interfaces/currentFilter.interface.ts index 67fcaa414..7e97f444c 100644 --- a/packages/common/src/interfaces/currentFilter.interface.ts +++ b/packages/common/src/interfaces/currentFilter.interface.ts @@ -13,4 +13,7 @@ export interface CurrentFilter { /** Filter search terms */ searchTerms?: SearchTerm[]; + + /** Target element selector from which the filter was triggered from. */ + targetSelector?: string; } diff --git a/packages/common/src/interfaces/filterChangedArgs.interface.ts b/packages/common/src/interfaces/filterChangedArgs.interface.ts index 17fc065ee..b5978cd07 100644 --- a/packages/common/src/interfaces/filterChangedArgs.interface.ts +++ b/packages/common/src/interfaces/filterChangedArgs.interface.ts @@ -13,4 +13,5 @@ export interface FilterChangedArgs { operator: OperatorType | OperatorString; searchTerms: SearchTerm[]; shouldTriggerQuery?: boolean; + targetSelector?: string; } diff --git a/packages/common/src/interfaces/searchColumnFilter.interface.ts b/packages/common/src/interfaces/searchColumnFilter.interface.ts index fd014d3a7..12a2de43f 100644 --- a/packages/common/src/interfaces/searchColumnFilter.interface.ts +++ b/packages/common/src/interfaces/searchColumnFilter.interface.ts @@ -32,4 +32,7 @@ export interface SearchColumnFilter { /** What is the Field Type that can be used by the Filter (as precedence over the "type" set the column definition) */ type: typeof FieldType[keyof typeof FieldType]; + + /** Target element selector from which the filter was triggered from. */ + targetSelector?: string; } diff --git a/packages/common/src/services/__tests__/domUtilities.spec.ts b/packages/common/src/services/__tests__/domUtilities.spec.ts index 8f861c55a..e95e6ef2c 100644 --- a/packages/common/src/services/__tests__/domUtilities.spec.ts +++ b/packages/common/src/services/__tests__/domUtilities.spec.ts @@ -6,6 +6,7 @@ import { findFirstElementAttribute, getElementOffsetRelativeToParent, getHtmlElementOffset, + getSelectorStringFromElement, htmlEncode, htmlEntityDecode, sanitizeHtmlToText, @@ -130,6 +131,37 @@ describe('Service/domUtilies', () => { }); }); + describe('getSelectorStringFromElement() method', () => { + it('should return html element selector without classes when div is created without classes', () => { + const result = getSelectorStringFromElement(null); + + expect(result).toBe(''); + }); + + it('should return html element selector without classes when div is created without classes', () => { + const tmpDiv = document.createElement('div'); + const result = getSelectorStringFromElement(tmpDiv); + + expect(result).toBe('div'); + }); + + it('should return html element selector with a single class name when exists', () => { + const tmpDiv = document.createElement('div'); + tmpDiv.className = 'some-class' + const result = getSelectorStringFromElement(tmpDiv); + + expect(result).toBe('div.some-class'); + }); + + it('should return html element selector with multiple classes when exists', () => { + const tmpDiv = document.createElement('div'); + tmpDiv.className = 'some-class other-class yet-more' + const result = getSelectorStringFromElement(tmpDiv); + + expect(result).toBe('div.some-class.other-class.yet-more'); + }); + }); + describe('htmlEncode method', () => { it('should return a encoded HTML string', () => { const result = htmlEncode(`
Something
`); diff --git a/packages/common/src/services/__tests__/filter.service.spec.ts b/packages/common/src/services/__tests__/filter.service.spec.ts index c203a82ce..181f4e4cc 100644 --- a/packages/common/src/services/__tests__/filter.service.spec.ts +++ b/packages/common/src/services/__tests__/filter.service.spec.ts @@ -314,6 +314,8 @@ describe('FilterService', () => { model: Filters.select, searchTerms: [true], collection: [{ value: true, label: 'True' }, { value: false, label: 'False' }], } } as Column; + const tmpDivElm = document.createElement('div'); + tmpDivElm.className = 'some-classes'; const mockHeaderArgs = { grid: gridStub, column: mockColumn, node: document.getElementById(DOM_ELEMENT_ID), }; const mockSearchArgs = { clearFilterTriggered: false, @@ -322,6 +324,7 @@ describe('FilterService', () => { columnDef: { id: 'firstName', field: 'firstName', filterable: true } as Column, operator: 'EQ', searchTerms: ['John'], + target: tmpDivElm, grid: gridStub }; sharedService.allColumns = [mockColumn]; @@ -352,7 +355,7 @@ describe('FilterService', () => { }); it('should execute the search callback normally when a input change event is triggered and searchTerms are defined', (done) => { - const expectationColumnFilter = { columnDef: mockColumn, columnId: 'firstName', operator: 'EQ', searchTerms: ['John'], parsedSearchTerms: ['John'], type: FieldType.string }; + const expectationColumnFilter = { columnDef: mockColumn, columnId: 'firstName', operator: 'EQ', searchTerms: ['John'], parsedSearchTerms: ['John'], targetSelector: '', type: FieldType.string }; const spySearchChange = jest.spyOn(service.onSearchChange as any, 'notify'); const spyEmit = jest.spyOn(service, 'emitFilterChanged'); @@ -381,7 +384,7 @@ describe('FilterService', () => { }); it('should execute the callback normally when a input change event is triggered and the searchTerm comes from this event.target', () => { - const expectationColumnFilter = { columnDef: mockColumn, columnId: 'firstName', operator: 'EQ', searchTerms: ['John'], parsedSearchTerms: ['John'], type: FieldType.string }; + const expectationColumnFilter = { columnDef: mockColumn, columnId: 'firstName', operator: 'EQ', searchTerms: ['John'], parsedSearchTerms: ['John'], targetSelector: '', type: FieldType.string }; const spySearchChange = jest.spyOn(service.onSearchChange as any, 'notify'); sharedService.allColumns = [mockColumn]; @@ -418,7 +421,7 @@ describe('FilterService', () => { }); it('should NOT delete the column filters entry (from column filter object) even when searchTerms is empty when user set `emptySearchTermReturnAllValues` to False', () => { - const expectationColumnFilter = { columnDef: mockColumn, columnId: 'firstName', operator: 'EQ', searchTerms: [''], parsedSearchTerms: [''], type: FieldType.string }; + const expectationColumnFilter = { columnDef: mockColumn, columnId: 'firstName', operator: 'EQ', searchTerms: [''], parsedSearchTerms: [''], targetSelector: 'div.some-classes', type: FieldType.string }; const spySearchChange = jest.spyOn(service.onSearchChange as any, 'notify'); sharedService.allColumns = [mockColumn]; @@ -426,7 +429,11 @@ describe('FilterService', () => { service.bindLocalOnFilter(gridStub); mockArgs.column.filter = { emptySearchTermReturnAllValues: false }; gridStub.onHeaderRowCellRendered.notify(mockArgs as any, new Slick.EventData(), gridStub); - service.getFiltersMetadata()[0].callback(new Event('input'), { columnDef: mockColumn, operator: 'EQ', searchTerms: [''], shouldTriggerQuery: true }); + const tmpDivElm = document.createElement('div'); + tmpDivElm.className = 'some-classes'; + const inputEvent = new Event('input'); + Object.defineProperty(inputEvent, 'target', { writable: true, configurable: true, value: tmpDivElm }); + service.getFiltersMetadata()[0].callback(inputEvent, { columnDef: mockColumn, operator: 'EQ', searchTerms: [''], shouldTriggerQuery: true, target: tmpDivElm }); expect(service.getColumnFilters()).toContainEntry(['firstName', expectationColumnFilter]); expect(spySearchChange).toHaveBeenCalledWith({ @@ -439,9 +446,9 @@ describe('FilterService', () => { searchTerms: [''], parsedSearchTerms: [''], grid: gridStub, - target: null, + target: tmpDivElm, }, expect.anything()); - expect(service.getCurrentLocalFilters()).toEqual([{ columnId: 'firstName', operator: 'EQ', searchTerms: [''] }]); + expect(service.getCurrentLocalFilters()).toEqual([{ columnId: 'firstName', operator: 'EQ', searchTerms: [''], targetSelector: 'div.some-classes', }]); }); it('should delete the column filters entry (from column filter object) when searchTerms is empty array and even when triggered event is undefined', () => { @@ -495,7 +502,7 @@ describe('FilterService', () => { describe('clearFilterByColumnId method', () => { it('should clear the filter by passing a column id as argument on a backend grid', async () => { - const filterExpectation = { columnDef: mockColumn2, columnId: 'lastName', operator: 'NE', searchTerms: ['Doe'], parsedSearchTerms: ['Doe'], type: FieldType.string }; + const filterExpectation = { columnDef: mockColumn2, columnId: 'lastName', operator: 'NE', searchTerms: ['Doe'], parsedSearchTerms: ['Doe'], targetSelector: '', type: FieldType.string }; const newEvent = new CustomEvent(`mouseup`); const spyClear = jest.spyOn(service.getFiltersMetadata()[0], 'clear'); const spyFilterChange = jest.spyOn(service, 'onBackendFilterChange'); @@ -516,8 +523,8 @@ describe('FilterService', () => { }); it('should not call "onBackendFilterChange" method when the filter is previously empty', async () => { - const filterFirstExpectation = { columnDef: mockColumn1, columnId: 'firstName', operator: 'EQ', searchTerms: ['John'], parsedSearchTerms: ['John'], type: FieldType.string }; - const filterLastExpectation = { columnDef: mockColumn2, columnId: 'lastName', operator: 'NE', searchTerms: ['Doe'], parsedSearchTerms: ['Doe'], type: FieldType.string }; + const filterFirstExpectation = { columnDef: mockColumn1, columnId: 'firstName', operator: 'EQ', searchTerms: ['John'], parsedSearchTerms: ['John'], targetSelector: '', type: FieldType.string }; + const filterLastExpectation = { columnDef: mockColumn2, columnId: 'lastName', operator: 'NE', searchTerms: ['Doe'], parsedSearchTerms: ['Doe'], targetSelector: '', type: FieldType.string }; const newEvent = new Event('mouseup'); const spyClear = jest.spyOn(service.getFiltersMetadata()[2], 'clear'); const spyFilterChange = jest.spyOn(service, 'onBackendFilterChange'); @@ -564,7 +571,7 @@ describe('FilterService', () => { expect(service.getColumnFilters()).toEqual({}); expect(spyFilterChange).not.toHaveBeenCalled(); expect(spyEmitter).not.toHaveBeenCalled(); - expect(sharedService.columnDefinitions[0].filter.searchTerms).toBeUndefined(); + expect(sharedService.columnDefinitions[0].filter!.searchTerms).toBeUndefined(); }); it('should execute the "onError" method when the Promise throws an error', (done) => { @@ -597,7 +604,7 @@ describe('FilterService', () => { gridOptionMock.backendServiceApi!.onError = () => jest.fn(); const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish'); const spyOnError = jest.spyOn(gridOptionMock.backendServiceApi as BackendServiceApi, 'onError'); - jest.spyOn(gridOptionMock.backendServiceApi, 'process').mockReturnValue(throwError(errorExpected)); + jest.spyOn(gridOptionMock.backendServiceApi as BackendServiceApi, 'process').mockReturnValue(throwError(errorExpected)); backendUtilityService.addRxJsResource(rxjsResourceStub); service.addRxJsResource(rxjsResourceStub); @@ -648,7 +655,7 @@ describe('FilterService', () => { expect(spyClear).toHaveBeenCalled(); expect(filterCountBefore).toBe(2); expect(filterCountAfter).toBe(1); - expect(service.getColumnFilters()).toEqual({ lastName: { columnDef: mockColumn2, columnId: 'lastName', operator: 'NE', searchTerms: ['Doe'], parsedSearchTerms: ['Doe'], type: FieldType.string } }); + expect(service.getColumnFilters()).toEqual({ lastName: { columnDef: mockColumn2, columnId: 'lastName', operator: 'NE', searchTerms: ['Doe'], parsedSearchTerms: ['Doe'], targetSelector: '', type: FieldType.string } }); expect(spyEmitter).toHaveBeenCalledWith('local'); }); }); @@ -709,7 +716,7 @@ describe('FilterService', () => { service.init(gridStub); const parsedSearchTerms = getParsedSearchTermsByFieldType(searchTerms, 'text'); - const columnFilters = { firstName: { columnDef: undefined, columnId: 'firstName', operator: 'EQ', searchTerms, parsedSearchTerms, type: FieldType.string } } as ColumnFilters; + const columnFilters = { firstName: { columnDef: undefined as any, columnId: 'firstName', operator: 'EQ', searchTerms, parsedSearchTerms, targetSelector: '', type: FieldType.string } } as ColumnFilters; const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); expect(output).toBe(true); @@ -836,7 +843,7 @@ describe('FilterService', () => { const columnFilter = { columnDef: mockColumn1, columnId: 'firstName', type: FieldType.string }; const filterCondition = service.parseFormInputFilterConditions(searchTerms, columnFilter); const parsedSearchTerms = getParsedSearchTermsByFieldType(filterCondition.searchTerms, 'text'); - const columnFilters = { firstName: { ...columnFilter, operator: filterCondition.operator, searchTerms: filterCondition.searchTerms, parsedSearchTerms } }; + const columnFilters = { firstName: { ...columnFilter, operator: filterCondition.operator, searchTerms: filterCondition.searchTerms, parsedSearchTerms } } as ColumnFilters; const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); expect(output).toBe(true); @@ -851,7 +858,7 @@ describe('FilterService', () => { const columnFilter = { columnDef: mockColumn1, columnId: 'firstName', type: FieldType.string }; const filterCondition = service.parseFormInputFilterConditions(searchTerms, columnFilter); const parsedSearchTerms = getParsedSearchTermsByFieldType(filterCondition.searchTerms, 'text'); - const columnFilters = { firstName: { ...columnFilter, operator: filterCondition.operator, searchTerms: filterCondition.searchTerms, parsedSearchTerms } }; + const columnFilters = { firstName: { ...columnFilter, operator: filterCondition.operator, searchTerms: filterCondition.searchTerms, parsedSearchTerms } } as ColumnFilters; const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); expect(output).toBe(true); @@ -866,7 +873,7 @@ describe('FilterService', () => { const columnFilter = { columnDef: mockColumn1, columnId: 'firstName', type: FieldType.string }; const filterCondition = service.parseFormInputFilterConditions(searchTerms, columnFilter); const parsedSearchTerms = getParsedSearchTermsByFieldType(filterCondition.searchTerms, 'text'); - const columnFilters = { firstName: { ...columnFilter, operator: filterCondition.operator, searchTerms: filterCondition.searchTerms, parsedSearchTerms } }; + const columnFilters = { firstName: { ...columnFilter, operator: filterCondition.operator, searchTerms: filterCondition.searchTerms, parsedSearchTerms } } as ColumnFilters; const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); expect(output).toBe(true); @@ -976,7 +983,7 @@ describe('FilterService', () => { }); it('should throw an error when grid argument is undefined', (done) => { - service.onBackendFilterChange(undefined as any, undefined).catch((error) => { + service.onBackendFilterChange(undefined as any, undefined as any).catch((error) => { expect(error.message).toContain(`Something went wrong when trying to bind the "onBackendFilterChange(event, args)" function`); done(); }); @@ -1311,14 +1318,14 @@ describe('FilterService', () => { expect(emitSpy).toHaveBeenCalledWith('local'); expect(clearSpy).toHaveBeenCalledWith(false); expect(service.getColumnFilters()).toEqual({ - firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', parsedSearchTerms: ['Jane'], type: FieldType.string }, + firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', parsedSearchTerms: ['Jane'], targetSelector: '', type: FieldType.string }, }); expect(spySearchChange).toHaveBeenCalledWith({ clearFilterTriggered: undefined, shouldTriggerQuery: true, columnId: 'firstName', columnDef: mockColumn1, - columnFilters: { firstName: { columnDef: mockColumn1, columnId: 'firstName', operator: 'StartsWith', searchTerms: ['Jane'], parsedSearchTerms: ['Jane'], type: FieldType.string } }, + columnFilters: { firstName: { columnDef: mockColumn1, columnId: 'firstName', operator: 'StartsWith', searchTerms: ['Jane'], parsedSearchTerms: ['Jane'], targetSelector: '', type: FieldType.string } }, operator: 'StartsWith', searchTerms: ['Jane'], parsedSearchTerms: ['Jane'], @@ -1341,14 +1348,14 @@ describe('FilterService', () => { expect(emitSpy).toHaveBeenCalledWith('local'); expect(clearSpy).toHaveBeenCalledWith(false); expect(service.getColumnFilters()).toEqual({ - firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', parsedSearchTerms: ['Jane'], type: FieldType.string }, + firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', parsedSearchTerms: ['Jane'], targetSelector: '', type: FieldType.string }, }); expect(spySearchChange).toHaveBeenCalledWith({ clearFilterTriggered: undefined, shouldTriggerQuery: true, columnId: 'firstName', columnDef: mockColumn1, - columnFilters: { firstName: { columnDef: mockColumn1, columnId: 'firstName', operator: 'StartsWith', searchTerms: ['Jane'], parsedSearchTerms: ['Jane'], type: FieldType.string } }, + columnFilters: { firstName: { columnDef: mockColumn1, columnId: 'firstName', operator: 'StartsWith', searchTerms: ['Jane'], parsedSearchTerms: ['Jane'], targetSelector: '', type: FieldType.string } }, operator: 'StartsWith', searchTerms: ['Jane'], parsedSearchTerms: ['Jane'], @@ -1650,7 +1657,7 @@ describe('FilterService', () => { const filterElm = document.body.querySelector(`#${DOM_ELEMENT_ID}`); expect(filterElm).toBeTruthy(); - expect(columnFilterMetadada.columnDef.id).toBe('name'); + expect(columnFilterMetadada!.columnDef.id).toBe('name'); }); it('should Draw DOM Element Filter on custom HTML element by string id with searchTerms', async () => { @@ -1664,7 +1671,7 @@ describe('FilterService', () => { const filterElm = document.body.querySelector(`#${DOM_ELEMENT_ID}`); expect(filterElm).toBeTruthy(); - expect(columnFilterMetadada.columnDef.id).toBe('firstName'); + expect(columnFilterMetadada!.columnDef.id).toBe('firstName'); }); it('should Draw DOM Element Filter on custom HTML element by HTMLDivElement', async () => { @@ -1674,12 +1681,12 @@ describe('FilterService', () => { gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub); await service.updateFilters(mockNewFilters); - const filterContainerElm: HTMLDivElement = document.querySelector(`#${DOM_ELEMENT_ID}`); + const filterContainerElm = document.querySelector(`#${DOM_ELEMENT_ID}`) as HTMLDivElement; const columnFilterMetadada = service.drawFilterTemplate('isActive', filterContainerElm); const filterElm = document.body.querySelector(`#${DOM_ELEMENT_ID}`); expect(filterElm).toBeTruthy(); - expect(columnFilterMetadada.columnDef.id).toBe('isActive'); + expect(columnFilterMetadada!.columnDef.id).toBe('isActive'); }); it('should Draw DOM Element Filter on custom HTML element return null', async () => { @@ -1689,7 +1696,7 @@ describe('FilterService', () => { gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub); await service.updateFilters(mockNewFilters); - const filterContainerElm: HTMLDivElement = document.querySelector(`#${DOM_ELEMENT_ID}`); + const filterContainerElm = document.querySelector(`#${DOM_ELEMENT_ID}`) as HTMLDivElement; const columnFilterMetadada1 = service.drawFilterTemplate('selector', filterContainerElm); const columnFilterMetadada2 = service.drawFilterTemplate('name', `#not-exists`); const columnFilterMetadada3 = service.drawFilterTemplate('invalid-column', filterContainerElm); @@ -1797,7 +1804,7 @@ describe('FilterService', () => { gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub); gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub); - const columnFilters = { file: { columnDef: mockColumn1, columnId: 'file', operator: 'Contains', searchTerms: ['map'], parsedSearchTerms: ['map'], type: FieldType.string } } as ColumnFilters; + const columnFilters = { file: { columnDef: mockColumn1, columnId: 'file', operator: 'Contains', searchTerms: ['map'], parsedSearchTerms: ['map'], targetSelector: '', type: FieldType.string } } as ColumnFilters; await service.updateFilters([{ columnId: 'file', operator: '', searchTerms: ['map'] }], true, true, true); const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); @@ -1822,7 +1829,7 @@ describe('FilterService', () => { gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub); gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub); - const columnFilters = { file: { columnDef: mockColumn1, columnId: 'file', operator: 'Contains', searchTerms: ['map'], parsedSearchTerms: ['map'], type: FieldType.string } } as ColumnFilters; + const columnFilters = { file: { columnDef: mockColumn1, columnId: 'file', operator: 'Contains', searchTerms: ['map'], parsedSearchTerms: ['map'], targetSelector: '', type: FieldType.string } } as ColumnFilters; await service.updateFilters([{ columnId: 'file', operator: '', searchTerms: ['map'] }], true, true, true); const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); @@ -1847,7 +1854,7 @@ describe('FilterService', () => { gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub); gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub); - const columnFilters = { file: { columnDef: mockColumn1, columnId: 'file', searchTerms: ['unknown'], type: FieldType.string } } as ColumnFilters; + const columnFilters = { file: { columnDef: mockColumn1, columnId: 'file', searchTerms: ['unknown'], targetSelector: '', type: FieldType.string } } as ColumnFilters; await service.updateFilters([{ columnId: 'file', operator: '', searchTerms: ['unknown'] }], true, true, true); const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); @@ -1893,14 +1900,14 @@ describe('FilterService', () => { .mockReturnValueOnce(dataset[6]); const mockItem1 = { __parentId: 9, __treeLevel: 2, dateModified: '2015-02-26T16:50:00.123Z', file: 'todo.txt', id: 10, size: 0.4 }; - gridOptionMock.treeDataOptions.excludeChildrenWhenFilteringTree = false; + gridOptionMock.treeDataOptions!.excludeChildrenWhenFilteringTree = false; 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); - const columnFilters = { file: { columnDef: mockColumn1, columnId: 'file', operator: 'Contains', searchTerms: ['misc'], parsedSearchTerms: ['misc'], type: FieldType.string } } as ColumnFilters; + const columnFilters = { file: { columnDef: mockColumn1, columnId: 'file', operator: 'Contains', searchTerms: ['misc'], parsedSearchTerms: ['misc'], targetSelector: '', type: FieldType.string } } as ColumnFilters; await service.updateFilters([{ columnId: 'file', operator: '', searchTerms: ['misc'] }], true, true, true); const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); @@ -1919,14 +1926,14 @@ describe('FilterService', () => { .mockReturnValueOnce(dataset[6]); const mockItem1 = { __parentId: 9, __treeLevel: 2, dateModified: '2015-02-26T16:50:00.123Z', file: 'todo.txt', id: 10, size: 0.4 }; - gridOptionMock.treeDataOptions.excludeChildrenWhenFilteringTree = true; + gridOptionMock.treeDataOptions!.excludeChildrenWhenFilteringTree = true; 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); - const columnFilters = { file: { columnDef: mockColumn1, columnId: 'file', operator: 'Contains', searchTerms: ['misc'], parsedSearchTerms: ['misc'], type: FieldType.string } } as ColumnFilters; + const columnFilters = { file: { columnDef: mockColumn1, columnId: 'file', operator: 'Contains', searchTerms: ['misc'], parsedSearchTerms: ['misc'], targetSelector: '', type: FieldType.string } } as ColumnFilters; await service.updateFilters([{ columnId: 'file', operator: '', searchTerms: ['misc'] }], true, true, true); const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); @@ -1945,8 +1952,8 @@ describe('FilterService', () => { .mockReturnValueOnce(dataset[6]); const mockItem1 = { __parentId: 9, __treeLevel: 2, dateModified: '2015-02-26T16:50:00.123Z', file: 'todo.txt', id: 10, size: 0.4 }; - gridOptionMock.treeDataOptions.excludeChildrenWhenFilteringTree = false; - gridOptionMock.treeDataOptions.autoApproveParentItemWhenTreeColumnIsValid = true; + gridOptionMock.treeDataOptions!.excludeChildrenWhenFilteringTree = false; + gridOptionMock.treeDataOptions!.autoApproveParentItemWhenTreeColumnIsValid = true; service.init(gridStub); service.bindLocalOnFilter(gridStub); @@ -1954,7 +1961,7 @@ describe('FilterService', () => { gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub); const columnFilters = { - file: { columnDef: mockColumn1, columnId: 'file', operator: 'Contains', searchTerms: ['misc'], parsedSearchTerms: ['misc'], type: FieldType.string }, + file: { columnDef: mockColumn1, columnId: 'file', operator: 'Contains', searchTerms: ['misc'], parsedSearchTerms: ['misc'], targetSelector: '', type: FieldType.string }, } as ColumnFilters; await service.updateFilters([{ columnId: 'file', operator: '', searchTerms: ['misc'] }], true, true, true); const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); @@ -1974,8 +1981,8 @@ describe('FilterService', () => { .mockReturnValueOnce(dataset[6]); const mockItem1 = { __parentId: 9, __treeLevel: 2, dateModified: '2015-02-26T16:50:00.123Z', file: 'todo.txt', id: 10, size: 0.4 }; - gridOptionMock.treeDataOptions.excludeChildrenWhenFilteringTree = false; - gridOptionMock.treeDataOptions.autoApproveParentItemWhenTreeColumnIsValid = false; + gridOptionMock.treeDataOptions!.excludeChildrenWhenFilteringTree = false; + gridOptionMock.treeDataOptions!.autoApproveParentItemWhenTreeColumnIsValid = false; service.init(gridStub); service.bindLocalOnFilter(gridStub); @@ -1984,7 +1991,7 @@ describe('FilterService', () => { gridStub.onHeaderRowCellRendered.notify(mockArgs3 as any, new Slick.EventData(), gridStub); const columnFilters = { - size: { columnDef: mockColumn3, columnId: 'size', operator: '<', searchTerms: ['0.1'], parsedSearchTerms: ['0.1'], type: FieldType.string }, + size: { columnDef: mockColumn3, columnId: 'size', operator: '<', searchTerms: ['0.1'], parsedSearchTerms: ['0.1'], targetSelector: '', type: FieldType.string }, } as ColumnFilters; await service.updateFilters([{ columnId: 'size', operator: '<', searchTerms: ['0.1'] }], true, true, true); const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters }); diff --git a/packages/common/src/services/domUtilities.ts b/packages/common/src/services/domUtilities.ts index 9dce1c79e..534246b71 100644 --- a/packages/common/src/services/domUtilities.ts +++ b/packages/common/src/services/domUtilities.ts @@ -238,6 +238,14 @@ export function getHtmlElementOffset(element?: HTMLElement): HtmlElementPosition return { top, left, bottom, right }; } +export function getSelectorStringFromElement(elm?: HTMLElement | null) { + let selector = ''; + if (elm?.localName) { + selector = elm?.className ? `${elm.localName}.${Array.from(elm.classList).join('.')}` : elm.localName; + } + return selector; +} + export function findFirstElementAttribute(inputElm: Element | null | undefined, attributes: string[]): string | null { if (inputElm) { for (const attribute of attributes) { diff --git a/packages/common/src/services/filter.service.ts b/packages/common/src/services/filter.service.ts index 8550fe63b..32d5d6623 100644 --- a/packages/common/src/services/filter.service.ts +++ b/packages/common/src/services/filter.service.ts @@ -31,7 +31,7 @@ import { SlickNamespace, } from './../interfaces/index'; import { BackendUtilityService } from './backendUtility.service'; -import { sanitizeHtmlToText, } from '../services/domUtilities'; +import { getSelectorStringFromElement, sanitizeHtmlToText, } from '../services/domUtilities'; import { getDescendantProperty, mapOperatorByFieldType, } from './utilities'; import { SharedService } from './shared.service'; import { RxJsFacade, Subject } from './rxjsFacade'; @@ -669,6 +669,9 @@ export class FilterService { if (columnFilter.operator) { filter.operator = columnFilter.operator; } + if (columnFilter.targetSelector) { + filter.targetSelector = columnFilter.targetSelector; + } if (Array.isArray(filter.searchTerms) && filter.searchTerms.length > 0 && (!emptySearchTermReturnAllValues || filter.searchTerms[0] !== '')) { currentFilters.push(filter); } @@ -1127,7 +1130,8 @@ export class FilterService { columnId: colId, columnDef, parsedSearchTerms: [], - type: fieldType + type: fieldType, + targetSelector: getSelectorStringFromElement(event?.target as HTMLElement | undefined) }; const inputSearchConditions = this.parseFormInputFilterConditions(searchTerms, colFilter); colFilter.operator = operator || inputSearchConditions.operator || mapOperatorByFieldType(fieldType); diff --git a/packages/graphql/src/services/__tests__/graphql.service.spec.ts b/packages/graphql/src/services/__tests__/graphql.service.spec.ts index 7666bfa3c..0d5533134 100644 --- a/packages/graphql/src/services/__tests__/graphql.service.spec.ts +++ b/packages/graphql/src/services/__tests__/graphql.service.spec.ts @@ -521,7 +521,7 @@ describe('GraphqlService', () => { const querySpy = jest.spyOn(service, 'buildQuery'); const resetSpy = jest.spyOn(service, 'resetPaginationOptions'); const mockColumn = { id: 'gender', field: 'gender' } as Column; - const mockColumnFilter = { columnDef: mockColumn, columnId: 'gender', operator: 'EQ', searchTerms: ['female'] } as ColumnFilter; + const mockColumnFilter = { columnDef: mockColumn, columnId: 'gender', operator: 'EQ', searchTerms: ['female'], targetSelector: 'div.some-classes' } as ColumnFilter; const mockFilterChangedArgs = { columnDef: mockColumn, columnId: 'gender', @@ -529,7 +529,8 @@ describe('GraphqlService', () => { grid: gridStub, operator: 'EQ', searchTerms: ['female'], - shouldTriggerQuery: true + shouldTriggerQuery: true, + targetSelector: 'div.some-classes' } as FilterChangedArgs; service.init(serviceOptions, paginationOptions, gridStub); @@ -539,7 +540,7 @@ describe('GraphqlService', () => { expect(removeSpaces(query)).toBe(removeSpaces(expectation)); expect(querySpy).toHaveBeenCalled(); expect(resetSpy).toHaveBeenCalled(); - expect(currentFilters).toEqual([{ columnId: 'gender', operator: 'EQ', searchTerms: ['female'] }]); + expect(currentFilters).toEqual([{ columnId: 'gender', operator: 'EQ', searchTerms: ['female'], targetSelector: 'div.some-classes' }]); }); it('should return a query with a new filter when previous filters exists', () => { @@ -550,7 +551,7 @@ describe('GraphqlService', () => { const resetSpy = jest.spyOn(service, 'resetPaginationOptions'); const mockColumn = { id: 'gender', field: 'gender' } as Column; const mockColumnName = { id: 'firstName', field: 'firstName' } as Column; - const mockColumnFilter = { columnDef: mockColumn, columnId: 'gender', operator: 'EQ', searchTerms: ['female'] } as ColumnFilter; + const mockColumnFilter = { columnDef: mockColumn, columnId: 'gender', operator: 'EQ', searchTerms: ['female'], targetSelector: 'div.some-classes' } as ColumnFilter; const mockColumnFilterName = { columnDef: mockColumnName, columnId: 'firstName', operator: 'StartsWith', searchTerms: ['John'] } as ColumnFilter; const mockFilterChangedArgs = { columnDef: mockColumn, @@ -570,7 +571,7 @@ describe('GraphqlService', () => { expect(querySpy).toHaveBeenCalled(); expect(resetSpy).toHaveBeenCalled(); expect(currentFilters).toEqual([ - { columnId: 'gender', operator: 'EQ', searchTerms: ['female'] }, + { columnId: 'gender', operator: 'EQ', searchTerms: ['female'], targetSelector: 'div.some-classes' }, { columnId: 'firstName', operator: 'StartsWith', searchTerms: ['John'] } ]); }); diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index e5e49f1b4..1b73b1271 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -632,6 +632,9 @@ export class GraphqlService implements BackendService { if (filter.operator) { tmpFilter.operator = filter.operator; } + if (filter.targetSelector) { + tmpFilter.targetSelector = filter.targetSelector; + } if (Array.isArray(filter.searchTerms)) { tmpFilter.searchTerms = filter.searchTerms; } diff --git a/packages/odata/src/services/__tests__/grid-odata.service.spec.ts b/packages/odata/src/services/__tests__/grid-odata.service.spec.ts index c977751e2..d8dd70fee 100644 --- a/packages/odata/src/services/__tests__/grid-odata.service.spec.ts +++ b/packages/odata/src/services/__tests__/grid-odata.service.spec.ts @@ -294,7 +294,8 @@ describe('GridOdataService', () => { grid: gridStub, operator: 'EQ', searchTerms: ['female'], - shouldTriggerQuery: true + shouldTriggerQuery: true, + targetSelector: 'div.some-classes' } as FilterChangedArgs; service.init(serviceOptions, paginationOptions, gridStub); @@ -322,7 +323,8 @@ describe('GridOdataService', () => { grid: gridStub, operator: 'EQ', searchTerms: ['female'], - shouldTriggerQuery: true + shouldTriggerQuery: true, + targetSelector: 'div.some-classes' } as FilterChangedArgs; service.init(serviceOptions, paginationOptions, gridStub); @@ -356,7 +358,8 @@ describe('GridOdataService', () => { grid: gridStub, operator: 'EQ', searchTerms: ['female'], - shouldTriggerQuery: true + shouldTriggerQuery: true, + targetSelector: 'div.some-classes' } as FilterChangedArgs; service.init(serviceOptions, paginationOptions, gridStub); @@ -375,8 +378,8 @@ describe('GridOdataService', () => { const resetSpy = jest.spyOn(service, 'resetPaginationOptions'); const mockColumn = { id: 'gender', field: 'gender' } as Column; const mockColumnName = { id: 'firstName', field: 'firstName' } as Column; - const mockColumnFilter = { columnDef: mockColumn, columnId: 'gender', operator: 'EQ', searchTerms: ['female'] } as ColumnFilter; - const mockColumnFilterName = { columnDef: mockColumnName, columnId: 'firstName', operator: 'StartsWith', searchTerms: ['John'] } as ColumnFilter; + const mockColumnFilter = { columnDef: mockColumn, columnId: 'gender', operator: 'EQ', searchTerms: ['female'], targetSelector: 'div.some-classes' } as ColumnFilter; + const mockColumnFilterName = { columnDef: mockColumnName, columnId: 'firstName', operator: 'StartsWith', searchTerms: ['John'], targetSelector: 'div.some-classes' } as ColumnFilter; const mockFilterChangedArgs = { columnDef: mockColumn, columnId: 'gender', @@ -395,8 +398,8 @@ describe('GridOdataService', () => { expect(querySpy).toHaveBeenCalled(); expect(resetSpy).toHaveBeenCalled(); expect(currentFilters).toEqual([ - { columnId: 'gender', operator: 'EQ', searchTerms: ['female'] }, - { columnId: 'firstName', operator: 'StartsWith', searchTerms: ['John'] } + { columnId: 'gender', operator: 'EQ', searchTerms: ['female'], targetSelector: 'div.some-classes' }, + { columnId: 'firstName', operator: 'StartsWith', searchTerms: ['John'], targetSelector: 'div.some-classes' } ]); }); }); @@ -1798,7 +1801,7 @@ describe('GridOdataService', () => { serviceOptions.enableCount = true; service.init(serviceOptions, paginationOptions, gridStub); - service.postProcess({ '__count': 20 } ); + service.postProcess({ '__count': 20 }); expect(paginationOptions.totalItems).toBe(20); }); diff --git a/packages/odata/src/services/grid-odata.service.ts b/packages/odata/src/services/grid-odata.service.ts index 8489dcd1a..e62e23fba 100644 --- a/packages/odata/src/services/grid-odata.service.ts +++ b/packages/odata/src/services/grid-odata.service.ts @@ -590,6 +590,9 @@ export class GridOdataService implements BackendService { if (filter.operator) { tmpFilter.operator = filter.operator; } + if (filter.targetSelector) { + tmpFilter.targetSelector = filter.targetSelector; + } if (Array.isArray(filter.searchTerms)) { tmpFilter.searchTerms = filter.searchTerms; } diff --git a/test/cypress/e2e/example09.cy.js b/test/cypress/e2e/example09.cy.js index 0b7020443..8aed405da 100644 --- a/test/cypress/e2e/example09.cy.js +++ b/test/cypress/e2e/example09.cy.js @@ -599,7 +599,7 @@ describe('Example 09 - OData Grid', { retries: 1 }, () => { cy.window().then((win) => { // expect(win.console.log).to.have.callCount(2); - expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [{ columnId: 'name', operator: 'Contains', searchTerms: ['x'] }], type: 'filter' }); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [{ columnId: 'name', operator: 'Contains', searchTerms: ['x'], targetSelector: 'input.form-control.filter-name.compound-input' }], type: 'filter' }); // expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 1, pageSize: 10 }, type: 'pagination' }); }); }); diff --git a/test/cypress/e2e/example15.cy.js b/test/cypress/e2e/example15.cy.js index c9ab2c546..1bd1bfa42 100644 --- a/test/cypress/e2e/example15.cy.js +++ b/test/cypress/e2e/example15.cy.js @@ -563,7 +563,7 @@ describe('Example 15 - OData Grid using RxJS', { retries: 1 }, () => { cy.window().then((win) => { // expect(win.console.log).to.have.callCount(2); - expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [{ columnId: 'name', operator: 'Contains', searchTerms: ['x'] }], type: 'filter' }); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [{ columnId: 'name', operator: 'Contains', searchTerms: ['x'], targetSelector: 'input.form-control.filter-name.compound-input' }], type: 'filter' }); // expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 1, pageSize: 10 }, type: 'pagination' }); }); });