Skip to content

Commit a52e37c

Browse files
authored
fix(filter): input filter should only accept & use inline operator (#2040)
* fix(filter): input filter should only accept & use inline operator
1 parent b842c52 commit a52e37c

File tree

4 files changed

+84
-33
lines changed

4 files changed

+84
-33
lines changed

demos/vanilla/src/examples/example11.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -799,15 +799,17 @@ export default class Example11 {
799799
const { columns, filters, sorters, pinning } = currentGridState;
800800

801801
if (this.currentSelectedViewPreset && filters) {
802-
const filterName = (await prompt(`Update View name or click on OK to continue.`, this.currentSelectedViewPreset.label)) as string;
803-
this.currentSelectedViewPreset.label = filterName;
804-
this.currentSelectedViewPreset.value = filterName.replace(/\s/g, '');
805-
this.currentSelectedViewPreset.columns = columns || [];
806-
this.currentSelectedViewPreset.filters = filters || [];
807-
this.currentSelectedViewPreset.sorters = sorters || [];
808-
this.currentSelectedViewPreset.pinning = pinning || {};
809-
this.recreatePredefinedViews();
810-
localStorage.setItem('gridViewPreset', JSON.stringify(this.predefinedViews));
802+
const filterName = await prompt(`Update View name or click on OK to continue.`, this.currentSelectedViewPreset.label);
803+
if (filterName) {
804+
this.currentSelectedViewPreset.label = filterName;
805+
this.currentSelectedViewPreset.value = filterName.replace(/\s/g, '');
806+
this.currentSelectedViewPreset.columns = columns || [];
807+
this.currentSelectedViewPreset.filters = filters || [];
808+
this.currentSelectedViewPreset.sorters = sorters || [];
809+
this.currentSelectedViewPreset.pinning = pinning || {};
810+
this.recreatePredefinedViews();
811+
localStorage.setItem('gridViewPreset', JSON.stringify(this.predefinedViews));
812+
}
811813
}
812814
}
813815

packages/common/src/filters/__tests__/inputFilter.spec.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('InputFilter', () => {
3737
document.body.appendChild(divContainer);
3838
spyGetHeaderRow = vi.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer);
3939

40-
mockColumn = { id: 'duration', field: 'duration', filterable: true, filter: { model: Filters.input, operator: 'EQ' } };
40+
mockColumn = { id: 'duration', field: 'duration', filterable: true, filter: { model: Filters.input } };
4141
filterArguments = {
4242
grid: gridStub,
4343
columnDef: mockColumn,
@@ -128,7 +128,7 @@ describe('InputFilter', () => {
128128
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
129129

130130
expect(filterFilledElms.length).toBe(1);
131-
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['abc'], shouldTriggerQuery: true });
131+
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['=abc'], shouldTriggerQuery: true });
132132
});
133133

134134
it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableTrimWhiteSpace" is enabled in the column filter', () => {
@@ -167,7 +167,7 @@ describe('InputFilter', () => {
167167
const filledInputElm = divContainer.querySelector('.search-filter.filter-duration.filled') as HTMLInputElement;
168168

169169
expect(filledInputElm).toBeTruthy();
170-
expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: '>', searchTerms: ['>9'], shouldTriggerQuery: true });
170+
expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: '', searchTerms: ['>9'], shouldTriggerQuery: true });
171171
});
172172

173173
it('should call "setValues" and include an operator and expect the operator to show up in the output search string shown in the filter input text value', () => {
@@ -214,6 +214,24 @@ describe('InputFilter', () => {
214214

215215
filter.setValues('abc', 'a*');
216216
expect(filter.getValues()).toBe('abc*');
217+
218+
filter.setValues('abc', 'EQ');
219+
expect(filter.getValues()).toBe('=abc');
220+
221+
filter.setValues('abc', 'GE');
222+
expect(filter.getValues()).toBe('>=abc');
223+
224+
filter.setValues('abc', 'GT');
225+
expect(filter.getValues()).toBe('>abc');
226+
227+
filter.setValues('abc', 'NE');
228+
expect(filter.getValues()).toBe('!=abc');
229+
230+
filter.setValues('abc', 'LE');
231+
expect(filter.getValues()).toBe('<=abc');
232+
233+
filter.setValues('abc', 'LT');
234+
expect(filter.getValues()).toBe('<abc');
217235
});
218236
});
219237

@@ -227,7 +245,7 @@ describe('InputFilter', () => {
227245
filterElm.value = 'a';
228246
filterElm.dispatchEvent(new (window.window as any).Event('keyup', { key: 'a', keyCode: 97, bubbles: true, cancelable: true }));
229247

230-
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['a'], shouldTriggerQuery: true });
248+
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['a'], shouldTriggerQuery: true });
231249
});
232250

233251
it('should trigger the callback method with a delay when "filterTypingDebounce" is set in grid options and user types something in the input', () => {
@@ -243,7 +261,7 @@ describe('InputFilter', () => {
243261

244262
vi.advanceTimersByTime(2);
245263

246-
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['a'], shouldTriggerQuery: true });
264+
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['a'], shouldTriggerQuery: true });
247265
});
248266

249267
it('should trigger the callback method with a delay when BackendService is used with a "filterTypingDebounce" is set in grid options and user types something in the input', () => {
@@ -262,26 +280,28 @@ describe('InputFilter', () => {
262280

263281
vi.advanceTimersByTime(2);
264282

265-
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['a'], shouldTriggerQuery: true });
283+
expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '', searchTerms: ['a'], shouldTriggerQuery: true });
266284
});
267285

268286
it('should create the input filter with a default search term when passed as a filter argument', () => {
269287
filterArguments.searchTerms = ['xyz'];
288+
mockColumn.filter!.operator = 'EQ';
270289

271290
filter.init(filterArguments);
272291
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
273292

274-
expect(filterElm.value).toBe('xyz');
293+
expect(filterElm.value).toBe('=xyz');
275294
});
276295

277296
it('should expect the input not to have the "filled" css class when the search term provided is an empty string', () => {
278297
filterArguments.searchTerms = [''];
298+
mockColumn.filter!.operator = 'EQ';
279299

280300
filter.init(filterArguments);
281301
const filterElm = divContainer.querySelector('input.filter-duration') as HTMLInputElement;
282302
const filterFilledElms = divContainer.querySelectorAll<HTMLInputElement>('input.filter-duration.filled');
283303

284-
expect(filterElm.value).toBe('');
304+
expect(filterElm.value).toBe('=');
285305
expect(filterFilledElms.length).toBe(0);
286306
});
287307

packages/common/src/filters/__tests__/inputMaskFilter.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ describe('InputMaskFilter', () => {
223223
filter.init(filterArguments);
224224
const filterElm = divContainer.querySelector('input.filter-mask') as HTMLInputElement;
225225

226-
expect(filterElm.value).toBe('123');
226+
expect(filterElm.value).toBe('=123');
227227
});
228228

229229
it('should trigger a callback with the clear filter set when calling the "clear" method', () => {

packages/common/src/filters/inputFilter.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,18 @@ export class InputFilter implements Filter {
6666
return this.grid?.getOptions() ?? {};
6767
}
6868

69+
get isCompoundFilter(): boolean {
70+
return this.inputFilterType === 'compound';
71+
}
72+
6973
/**
7074
* Initialize the Filter
7175
*/
7276
init(args: FilterArguments): void {
7377
this.grid = args.grid;
7478
this.callback = args.callback;
7579
this.columnDef = args.columnDef;
76-
if (this.inputFilterType === 'compound') {
80+
if (this.isCompoundFilter) {
7781
this.operator = args.operator || '';
7882
}
7983
this.searchTerms = args?.searchTerms ?? [];
@@ -100,7 +104,7 @@ export class InputFilter implements Filter {
100104
this._bindEventService.bind(this._filterInputElm, 'wheel', this.onTriggerEvent.bind(this) as EventListener, {
101105
passive: true,
102106
});
103-
if (this.inputFilterType === 'compound' && this._selectOperatorElm) {
107+
if (this.isCompoundFilter && this._selectOperatorElm) {
104108
this._bindEventService.bind(this._selectOperatorElm, 'change', this.onTriggerEvent.bind(this) as EventListener);
105109
}
106110
}
@@ -115,7 +119,7 @@ export class InputFilter implements Filter {
115119
this._filterInputElm.value = '';
116120
this._currentValue = undefined;
117121
this.updateFilterStyle(false);
118-
if (this.inputFilterType === 'compound' && this._selectOperatorElm) {
122+
if (this.isCompoundFilter && this._selectOperatorElm) {
119123
this._selectOperatorElm.selectedIndex = 0;
120124
}
121125
this.onTriggerEvent(undefined, true);
@@ -140,7 +144,7 @@ export class InputFilter implements Filter {
140144
const searchValues = Array.isArray(values) ? values : [values];
141145
let newInputValue: SearchTerm = '';
142146
for (const value of searchValues) {
143-
if (this.inputFilterType === 'single') {
147+
if (!this.isCompoundFilter) {
144148
newInputValue = operator ? this.addOptionalOperatorIntoSearchString(value, operator) : value;
145149
} else {
146150
newInputValue = `${value}`;
@@ -194,6 +198,24 @@ export class InputFilter implements Filter {
194198
case '<=':
195199
searchTermPrefix = operator;
196200
break;
201+
case 'EQ':
202+
searchTermPrefix = '=';
203+
break;
204+
case 'GE':
205+
searchTermPrefix = '>=';
206+
break;
207+
case 'GT':
208+
searchTermPrefix = '>';
209+
break;
210+
case 'NE':
211+
searchTermPrefix = '!=';
212+
break;
213+
case 'LE':
214+
searchTermPrefix = '<=';
215+
break;
216+
case 'LT':
217+
searchTermPrefix = '<';
218+
break;
197219
case 'EndsWith':
198220
case '*z':
199221
searchTermPrefix = '*';
@@ -203,7 +225,7 @@ export class InputFilter implements Filter {
203225
searchTermSuffix = '*';
204226
break;
205227
}
206-
outputValue = `${searchTermPrefix}${outputValue}${searchTermSuffix}`;
228+
outputValue = `${searchTermPrefix}${this.trimValueWhenEnabled(outputValue)}${searchTermSuffix}`;
207229
}
208230

209231
return outputValue;
@@ -254,7 +276,10 @@ export class InputFilter implements Filter {
254276
placeholder = this.columnFilter.placeholder;
255277
}
256278

257-
const searchVal = `${searchTerm ?? ''}`;
279+
let searchVal = `${searchTerm ?? ''}`;
280+
if (!this.isCompoundFilter) {
281+
searchVal = this.addOptionalOperatorIntoSearchString(searchVal, this.operator);
282+
}
258283
this._filterInputElm = createDomElement('input', {
259284
type: this._inputType || 'text',
260285
autocomplete: 'off',
@@ -273,7 +298,7 @@ export class InputFilter implements Filter {
273298
}
274299

275300
// create the DOM Select dropdown for the Operator
276-
if (this.inputFilterType === 'single') {
301+
if (!this.isCompoundFilter) {
277302
this._filterContainerElm = this._filterInputElm;
278303
// append the new DOM element to the header row & an empty span
279304
this._filterInputElm.classList.add('search-filter', 'slick-filter');
@@ -307,6 +332,14 @@ export class InputFilter implements Filter {
307332
}
308333
}
309334

335+
protected trimValueWhenEnabled(val: string): string {
336+
const enableWhiteSpaceTrim = this.gridOptions.enableFilterTrimWhiteSpace || this.columnFilter.enableTrimWhiteSpace;
337+
if (typeof val === 'string' && enableWhiteSpaceTrim) {
338+
return val.trim();
339+
}
340+
return val;
341+
}
342+
310343
/**
311344
* Event handler to cover the following (keyup, change, mousewheel & spinner)
312345
* We will trigger the Filter Service callback from this handler
@@ -321,13 +354,9 @@ export class InputFilter implements Filter {
321354
this.updateFilterStyle(false);
322355
} else {
323356
const eventType = event?.type || '';
324-
const selectedOperator = (this._selectOperatorElm?.value ?? this.operator) as OperatorString;
325-
let value = this._filterInputElm.value;
326-
const enableWhiteSpaceTrim = this.gridOptions.enableFilterTrimWhiteSpace || this.columnFilter.enableTrimWhiteSpace;
327-
if (typeof value === 'string' && enableWhiteSpaceTrim) {
328-
value = value.trim();
329-
}
330-
357+
// pull operator from compound or re-evaluate on each keystroke
358+
const selectedOperator = (this._selectOperatorElm?.value ?? (!this.isCompoundFilter ? '' : this.operator)) as OperatorString;
359+
const value = this.trimValueWhenEnabled(this._filterInputElm.value);
331360
if ((event?.target as HTMLElement)?.tagName.toLowerCase() !== 'select') {
332361
this._currentValue = value;
333362
}
@@ -346,7 +375,7 @@ export class InputFilter implements Filter {
346375
const hasSkipNullValChanged =
347376
(skipNullInput && isDefined(this._currentValue)) || (this._currentValue === '' && isDefined(this._lastSearchValue));
348377

349-
if (this.inputFilterType === 'single' || !skipNullInput || hasSkipNullValChanged) {
378+
if (!this.isCompoundFilter || !skipNullInput || hasSkipNullValChanged) {
350379
if (typingDelay > 0) {
351380
window.clearTimeout(this._timer);
352381
this._timer = window.setTimeout(() => this.callback(event, callbackArgs), typingDelay);

0 commit comments

Comments
 (0)