• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 13390451169

18 Feb 2025 12:22PM CUT coverage: 91.688% (+0.07%) from 91.622%
13390451169

Pull #14647

github

web-flow
Merge 7d79199ab into 10ddb05cf
Pull Request #14647: feat(query-builder): support for nested queries and other improvements

13327 of 15595 branches covered (85.46%)

902 of 976 new or added lines in 19 files covered. (92.42%)

1 existing line in 1 file now uncovered.

26882 of 29319 relevant lines covered (91.69%)

33815.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.24
/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering.service.ts
1
import {
2
    Injectable,
3
    OnDestroy,
4
} from '@angular/core';
5
import { FilteringExpressionsTree, IFilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree';
6
import { IFilteringExpression, FilteringLogic } from '../../data-operations/filtering-expression.interface';
7
import { Subject } from 'rxjs';
8
import { takeUntil, first } from 'rxjs/operators';
9
import { IForOfState } from '../../directives/for-of/for_of.directive';
10
import { IFilteringOperation } from '../../data-operations/filtering-condition';
11
import { IColumnResizeEventArgs, IFilteringEventArgs } from '../common/events';
12
import { OverlayCancelableEventArgs, OverlaySettings, VerticalAlignment } from '../../services/overlay/utilities';
13
import { IgxOverlayService } from '../../services/overlay/overlay';
14
import { useAnimation } from '@angular/animations';
15
import { AbsoluteScrollStrategy } from '../../services/overlay/scroll/absolute-scroll-strategy';
16
import { IgxIconService } from '../../icon/icon.service';
17
import { editor, pinLeft, unpinLeft } from '@igniteui/material-icons-extended';
18
import { ExpressionUI, generateExpressionsList } from './excel-style/common';
19
import { ColumnType, GridType } from '../common/grid.interface';
20
import { formatDate } from '../../core/utils';
21
import { ExcelStylePositionStrategy } from './excel-style/excel-style-position-strategy';
22
import { fadeIn } from 'igniteui-angular/animations';
23
import { ExpressionsTreeUtil, isTree } from '../../data-operations/expressions-tree-util';
24

25
/**
26
 * @hidden
27
 */
28
@Injectable()
29
export class IgxFilteringService implements OnDestroy {
1✔
30
    public isFilterRowVisible = false;
4,058✔
31
    public filteredColumn: ColumnType = null;
4,058✔
32
    public selectedExpression: IFilteringExpression = null;
4,058✔
33
    public columnToMoreIconHidden = new Map<string, boolean>();
4,058✔
34
    public activeFilterCell = 0;
4,058✔
35
    public grid: GridType;
36

37
    private columnsWithComplexFilter = new Set<string>();
4,058✔
38
    private areEventsSubscribed = false;
4,058✔
39
    protected destroy$ = new Subject<boolean>();
4,058✔
40
    private isFiltering = false;
4,058✔
41
    private columnToExpressionsMap = new Map<string, ExpressionUI[]>();
4,058✔
42
    private columnStartIndex = -1;
4,058✔
43
    protected _filterMenuOverlaySettings: OverlaySettings = {
4,058✔
44
        closeOnEscape: true,
45
        closeOnOutsideClick: true,
46
        modal: false,
47
        positionStrategy: new ExcelStylePositionStrategy({
48
            verticalStartPoint: VerticalAlignment.Bottom,
49
            openAnimation: useAnimation(fadeIn, { params: { duration: '250ms' } }),
50
            closeAnimation: null
51
        }),
52
        scrollStrategy: new AbsoluteScrollStrategy()
53
    };
54
    protected lastActiveNode;
55

56
    constructor(
57
        private iconService: IgxIconService,
4,058✔
58
        protected _overlayService: IgxOverlayService,
4,058✔
59
    ) { }
60

61
    public ngOnDestroy(): void {
62
        this.destroy$.next(true);
3,898✔
63
        this.destroy$.complete();
3,898✔
64
    }
65

66
    public toggleFilterDropdown(element: HTMLElement, column: ColumnType) {
67

68
        const filterIcon = column.filteringExpressionsTree ? 'igx-excel-filter__icon--filtered' : 'igx-excel-filter__icon';
204✔
69
        const filterIconTarget = element.querySelector(`.${filterIcon}`) as HTMLElement || element;
204✔
70

71
        const id = this.grid.createFilterDropdown(column, {
204✔
72
            ...this._filterMenuOverlaySettings,
73
            ...{ target: filterIconTarget }
74
        });
75

76
        this._overlayService.opening
204✔
77
            .pipe(
78
                first(overlay => overlay.id === id),
204✔
79
                takeUntil(this.destroy$)
80
            )
81
            .subscribe((event: OverlayCancelableEventArgs) => {
82
                if (event.componentRef) {
204✔
83
                    event.componentRef.instance.initialize(column, this._overlayService);
183✔
84
                    event.componentRef.instance.overlayComponentId = id;
183✔
85
                }
86
                this.lastActiveNode = this.grid.navigation.activeNode;
204✔
87
            });
88

89
        this._overlayService.closed
204✔
90
            .pipe(
91
                first(overlay => overlay.id === id),
149✔
92
                takeUntil(this.destroy$)
93
            )
94
            .subscribe(() => {
95
                this._overlayService.detach(id);
84✔
96
                this.grid.navigation.activeNode = this.lastActiveNode;
84✔
97
                this.grid.theadRow.nativeElement.focus();
84✔
98
            });
99

100
        this._overlayService.show(id);
204✔
101
    }
102

103
    /**
104
     * Subscribe to grid's events.
105
     */
106
    public subscribeToEvents() {
107
        if (!this.areEventsSubscribed) {
3,232✔
108
            this.areEventsSubscribed = true;
554✔
109

110
            this.grid.columnResized.pipe(takeUntil(this.destroy$)).subscribe((eventArgs: IColumnResizeEventArgs) => {
554✔
111
                this.updateFilteringCell(eventArgs.column);
10✔
112
            });
113

114
            this.grid.parentVirtDir.chunkLoad.pipe(takeUntil(this.destroy$)).subscribe((eventArgs: IForOfState) => {
554✔
115
                if (eventArgs.startIndex !== this.columnStartIndex) {
5✔
116
                    this.columnStartIndex = eventArgs.startIndex;
5✔
117
                    this.grid.filterCellList.forEach((filterCell) => {
5✔
118
                        filterCell.updateFilterCellArea();
17✔
119
                    });
120
                }
121
            });
122

123
            this.grid.columnMovingEnd.pipe(takeUntil(this.destroy$)).subscribe(() => {
554✔
124
                this.grid.filterCellList.forEach((filterCell) => {
20✔
125
                    filterCell.updateFilterCellArea();
21✔
126
                });
127
            });
128
        }
129
    }
130

131
    /**
132
     * Close filtering row if a column is hidden.
133
     */
134
    public hideFilteringRowOnColumnVisibilityChange(col: ColumnType) {
135
        const filteringRow = this.grid?.filteringRow;
1,125✔
136

137
        if (filteringRow && filteringRow.column && filteringRow.column === col) {
1,125✔
138
            filteringRow.close();
1✔
139
        }
140
    }
141

142
    /**
143
     * Internal method to create expressionsTree and filter grid used in both filter modes.
144
     */
145
    public filterInternal(field: string, expressions: FilteringExpressionsTree | Array<ExpressionUI> = null): void {
70✔
146
        this.isFiltering = true;
121✔
147

148
        let expressionsTree;
149
        if (expressions && 'operator' in expressions) {
121✔
150
            expressionsTree = expressions;
38✔
151
        } else {
152
            expressionsTree = this.createSimpleFilteringTree(field, expressions);
83✔
153
        }
154

155
        if (expressionsTree.filteringOperands.length === 0) {
121✔
156
            this.clearFilter(field);
5✔
157
        } else {
158
            this.filter(field, null, expressionsTree);
116✔
159
        }
160

161
        this.isFiltering = false;
121✔
162
    }
163

164
    /**
165
     * Execute filtering on the grid.
166
     */
167
    public filter(field: string, value: any, conditionOrExpressionTree?: IFilteringOperation | IFilteringExpressionsTree,
168
        ignoreCase?: boolean) {
169

170
        const grid = this.grid;
394✔
171

172
        const col = grid.getColumnByName(field);
394✔
173
        const filteringIgnoreCase = ignoreCase || (col ? col.filteringIgnoreCase : false);
394✔
174

175
        const filteringTree = grid.filteringExpressionsTree;
394✔
176
        const columnFilteringExpressionsTree = ExpressionsTreeUtil.find(filteringTree, field) as IFilteringExpressionsTree;
394✔
177
        conditionOrExpressionTree = conditionOrExpressionTree ?? columnFilteringExpressionsTree;
394✔
178
        const fieldFilterIndex = ExpressionsTreeUtil.findIndex(filteringTree, field);
394✔
179

180
        const newFilteringTree: FilteringExpressionsTree =
181
            this.prepare_filtering_expression(filteringTree, field, value, conditionOrExpressionTree,
394✔
182
                filteringIgnoreCase, fieldFilterIndex, true);
183

184
        const eventArgs: IFilteringEventArgs = {
394✔
185
            owner: grid,
186
            filteringExpressions: ExpressionsTreeUtil.find(newFilteringTree, field) as FilteringExpressionsTree, cancel: false
187
        };
188
        this.grid.filtering.emit(eventArgs);
394✔
189

190
        if (eventArgs.cancel) {
394!
191
            return;
×
192
        }
193

194
        if (conditionOrExpressionTree) {
394✔
195
            this.filter_internal(field, value, conditionOrExpressionTree, filteringIgnoreCase);
393✔
196
        } else {
197
            const expressionsTreeForColumn = ExpressionsTreeUtil.find(this.grid.filteringExpressionsTree, field);
1✔
198
            if (!expressionsTreeForColumn) {
1!
199
                throw new Error('Invalid condition or Expression Tree!');
1✔
NEW
200
            } else if (isTree(expressionsTreeForColumn)) {
×
201
                this.filter_internal(field, value, expressionsTreeForColumn, filteringIgnoreCase);
×
202
            } else {
NEW
203
                this.filter_internal(field, value, expressionsTreeForColumn.condition, filteringIgnoreCase);
×
204
            }
205
        }
206
        const doneEventArgs = ExpressionsTreeUtil.find(this.grid.filteringExpressionsTree, field) as FilteringExpressionsTree;
393✔
207
        // Wait for the change detection to update filtered data through the pipes and then emit the event.
208
        requestAnimationFrame(() => this.grid.filteringDone.emit(doneEventArgs));
393✔
209
    }
210

211
    public filter_global(term, condition, ignoreCase) {
212
        if (!condition) {
×
213
            return;
×
214
        }
215

216
        const filteringTree = this.grid.filteringExpressionsTree;
×
217
        this.grid.crudService.endEdit(false);
×
218
        this.grid.page = 0;
×
219

220
        filteringTree.filteringOperands = [];
×
221
        for (const column of this.grid.columns) {
×
222
            this.prepare_filtering_expression(filteringTree, column.field, term,
×
223
                condition, ignoreCase || column.filteringIgnoreCase);
×
224
        }
225

226
        this.grid.filteringExpressionsTree = filteringTree;
×
227
    }
228

229
    /**
230
     * Clears the filter of a given column if name is provided. Otherwise clears the filters of all columns.
231
     */
232
    public clearFilter(field: string): void {
233
        if (field) {
193✔
234
            const column = this.grid.getColumnByName(field);
147✔
235
            if (!column) {
147✔
236
                return;
2✔
237
            }
238
        }
239

240
        const emptyFilter = new FilteringExpressionsTree(null, field);
191✔
241
        const onFilteringEventArgs: IFilteringEventArgs = {
191✔
242
            owner: this.grid,
243
            filteringExpressions: emptyFilter,
244
            cancel: false
245
        };
246

247
        this.grid.filtering.emit(onFilteringEventArgs);
191✔
248

249
        if (onFilteringEventArgs.cancel) {
191!
250
            return;
×
251
        }
252

253
        this.isFiltering = true;
191✔
254
        this.clear_filter(field);
191✔
255

256
        // Wait for the change detection to update filtered data through the pipes and then emit the event.
257
        requestAnimationFrame(() => this.grid.filteringDone.emit(emptyFilter));
191✔
258

259
        if (field) {
191✔
260
            const expressions = this.getExpressions(field);
145✔
261
            expressions.length = 0;
145✔
262
        } else {
263
            this.grid.columns.forEach(c => {
46✔
264
                const expressions = this.getExpressions(c.field);
207✔
265
                expressions.length = 0;
207✔
266
            });
267
        }
268

269
        this.isFiltering = false;
191✔
270
    }
271

272
    public clear_filter(fieldName: string) {
273
        const grid = this.grid;
324✔
274
        grid.crudService.endEdit(false);
324✔
275
        const filteringState = grid.filteringExpressionsTree;
324✔
276
        const index = ExpressionsTreeUtil.findIndex(filteringState, fieldName);
324✔
277

278
        if (index > -1) {
324✔
279
            filteringState.filteringOperands.splice(index, 1);
134✔
280
        } else if (!fieldName) {
190✔
281
            filteringState.filteringOperands = [];
46✔
282
        }
283

284
        grid.filteringExpressionsTree = filteringState;
324✔
285
    }
286

287
    /**
288
     * Filters all the `IgxColumnComponent` in the `IgxGridComponent` with the same condition.
289
     * @deprecated in version 19.0.0.
290
     */
291
    public filterGlobal(value: any, condition, ignoreCase?) {
292
        if (!condition) {
3✔
293
            return;
1✔
294
        }
295

296
        const filteringTree = this.grid.filteringExpressionsTree;
2✔
297
        const newFilteringTree = new FilteringExpressionsTree(filteringTree.operator, filteringTree.fieldName);
2✔
298

299
        for (const column of this.grid.columns) {
2✔
300
            this.prepare_filtering_expression(newFilteringTree, column.field, value, condition,
16✔
301
                ignoreCase || column.filteringIgnoreCase);
32✔
302
        }
303

304
        const eventArgs: IFilteringEventArgs = { owner: this.grid, filteringExpressions: newFilteringTree, cancel: false };
2✔
305
        this.grid.filtering.emit(eventArgs);
2✔
306
        if (eventArgs.cancel) {
2!
307
            return;
×
308
        }
309

310
        this.grid.crudService.endEdit(false);
2✔
311
        this.grid.page = 0;
2✔
312
        this.grid.filteringExpressionsTree = newFilteringTree;
2✔
313

314
        // Wait for the change detection to update filtered data through the pipes and then emit the event.
315
        requestAnimationFrame(() => this.grid.filteringDone.emit(this.grid.filteringExpressionsTree));
2✔
316
    }
317

318
    /**
319
     * Register filtering SVG icons in the icon service.
320
     */
321
    public registerSVGIcons(): void {
322
        const editorIcons = editor as any[];
1,015✔
323
        editorIcons.forEach(icon => {
1,015✔
324
            this.iconService.addSvgIconFromText(icon.name, icon.value, 'imx-icons');
60,900✔
325
        });
326
        this.iconService.addSvgIconFromText(pinLeft.name, pinLeft.value, 'imx-icons');
1,015✔
327
        this.iconService.addSvgIconFromText(unpinLeft.name, unpinLeft.value, 'imx-icons');
1,015✔
328
    }
329

330
    /**
331
     * Returns the ExpressionUI array for a given column.
332
     */
333
    public getExpressions(columnId: string): ExpressionUI[] {
334
        if (!this.columnToExpressionsMap.has(columnId)) {
40,882✔
335
            const column = this.grid.columns.find((col) => col.field === columnId);
12,990✔
336
            const expressionUIs = new Array<ExpressionUI>();
3,382✔
337
            if (column) {
3,382✔
338
                this.generateExpressionsList(column.filteringExpressionsTree, this.grid.filteringExpressionsTree.operator, expressionUIs);
3,380✔
339
                this.columnToExpressionsMap.set(columnId, expressionUIs);
3,380✔
340
            }
341
            return expressionUIs;
3,382✔
342
        }
343

344
        return this.columnToExpressionsMap.get(columnId);
37,500✔
345
    }
346

347
    /**
348
     * Recreates all ExpressionUIs for all columns. Executed after filtering to refresh the cache.
349
     */
350
    public refreshExpressions() {
351
        if (!this.isFiltering) {
2,726✔
352
            this.columnsWithComplexFilter.clear();
2,419✔
353

354
            this.columnToExpressionsMap.forEach((value: ExpressionUI[], key: string) => {
2,419✔
355
                const column = this.grid.columns.find((col) => col.field === key);
7,968✔
356
                if (column) {
1,838✔
357
                    value.length = 0;
1,828✔
358

359
                    this.generateExpressionsList(column.filteringExpressionsTree, this.grid.filteringExpressionsTree.operator, value);
1,828✔
360

361
                    const isComplex = this.isFilteringTreeComplex(column.filteringExpressionsTree);
1,828✔
362
                    if (isComplex) {
1,828!
363
                        this.columnsWithComplexFilter.add(key);
×
364
                    }
365

366
                    this.updateFilteringCell(column);
1,828✔
367
                } else {
368
                    this.columnToExpressionsMap.delete(key);
10✔
369
                }
370
            });
371
        }
372
    }
373

374
    /**
375
     * Remove an ExpressionUI for a given column.
376
     */
377
    public removeExpression(columnId: string, indexToRemove: number) {
378
        const expressionsList = this.getExpressions(columnId);
7✔
379

380
        if (indexToRemove === 0 && expressionsList.length > 1) {
7✔
381
            expressionsList[1].beforeOperator = null;
3✔
382
        } else if (indexToRemove === expressionsList.length - 1) {
4✔
383
            expressionsList[indexToRemove - 1].afterOperator = null;
2✔
384
        } else {
385
            expressionsList[indexToRemove - 1].afterOperator = expressionsList[indexToRemove + 1].beforeOperator;
2✔
386
            expressionsList[0].beforeOperator = null;
2✔
387
            expressionsList[expressionsList.length - 1].afterOperator = null;
2✔
388
        }
389

390
        expressionsList.splice(indexToRemove, 1);
7✔
391
    }
392

393
    /**
394
     * Generate filtering tree for a given column from existing ExpressionUIs.
395
     */
396
    public createSimpleFilteringTree(columnId: string, expressionUIList = null): FilteringExpressionsTree {
×
397
        const expressionsList = expressionUIList ? expressionUIList : this.getExpressions(columnId);
83✔
398
        const expressionsTree = new FilteringExpressionsTree(FilteringLogic.Or, columnId);
83✔
399
        let currAndBranch: FilteringExpressionsTree;
400

401
        for (const currExpressionUI of expressionsList) {
83✔
402
            if (!currExpressionUI.expression.condition.isUnary && currExpressionUI.expression.searchVal === null) {
98!
403
                if (currExpressionUI.afterOperator === FilteringLogic.And && !currAndBranch) {
×
404
                    currAndBranch = new FilteringExpressionsTree(FilteringLogic.And, columnId);
×
405
                    expressionsTree.filteringOperands.push(currAndBranch);
×
406
                }
407
                continue;
×
408
            }
409

410
            if ((currExpressionUI.beforeOperator === undefined || currExpressionUI.beforeOperator === null ||
98✔
411
                currExpressionUI.beforeOperator === FilteringLogic.Or) &&
412
                currExpressionUI.afterOperator === FilteringLogic.And) {
413

414
                currAndBranch = new FilteringExpressionsTree(FilteringLogic.And, columnId);
13✔
415
                expressionsTree.filteringOperands.push(currAndBranch);
13✔
416
                currAndBranch.filteringOperands.push(currExpressionUI.expression);
13✔
417

418
            } else if (currExpressionUI.beforeOperator === FilteringLogic.And) {
85✔
419
                currAndBranch.filteringOperands.push(currExpressionUI.expression);
17✔
420
            } else {
421
                expressionsTree.filteringOperands.push(currExpressionUI.expression);
68✔
422
                currAndBranch = null;
68✔
423
            }
424
        }
425

426
        return expressionsTree;
83✔
427
    }
428

429
    /**
430
     * Returns whether a complex filter is applied to a given column.
431
     */
432
    public isFilterComplex(columnId: string) {
433
        if (this.columnsWithComplexFilter.has(columnId)) {
1,091!
434
            return true;
×
435
        }
436

437
        const column = this.grid.columns.find((col) => col.field === columnId);
3,362✔
438
        const isComplex = column && this.isFilteringTreeComplex(column.filteringExpressionsTree);
1,091✔
439
        if (isComplex) {
1,091!
440
            this.columnsWithComplexFilter.add(columnId);
×
441
        }
442

443
        return isComplex;
1,091✔
444
    }
445

446
    /**
447
     * Returns the string representation of the FilteringLogic operator.
448
     */
449
    public getOperatorAsString(operator: FilteringLogic): any {
450
        if (operator === 0) {
459✔
451
            return this.grid.resourceStrings.igx_grid_filter_operator_and;
446✔
452
        } else {
453
            return this.grid.resourceStrings.igx_grid_filter_operator_or;
13✔
454
        }
455
    }
456

457
    /**
458
     * Generate the label of a chip from a given filtering expression.
459
     */
460
    public getChipLabel(expression: IFilteringExpression): any {
461
        if (expression.condition.isUnary) {
1,838✔
462
            return this.grid.resourceStrings[`igx_grid_filter_${expression.condition.name}`] || expression.condition.name;
378!
463
        } else if (expression.searchVal instanceof Date) {
1,460✔
464
            const column = this.grid.getColumnByName(expression.fieldName);
132✔
465
            const formatter = column.formatter;
132✔
466
            if (formatter) {
132!
467
                return formatter(expression.searchVal, undefined);
×
468
            }
469
            const pipeArgs = column.pipeArgs;
132✔
470
            return formatDate(expression.searchVal, pipeArgs.format, this.grid.locale);
132✔
471
        } else {
472
            return expression.searchVal;
1,328✔
473
        }
474
    }
475

476
    /**
477
     * Updates the content of a filterCell.
478
     */
479
    public updateFilteringCell(column: ColumnType) {
480
        const filterCell = column.filterCell;
1,861✔
481
        if (filterCell) {
1,861✔
482
            filterCell.updateFilterCellArea();
1,603✔
483
        }
484
    }
485

486
    public generateExpressionsList(expressions: IFilteringExpressionsTree | IFilteringExpression,
487
        operator: FilteringLogic,
488
        expressionsUIs: ExpressionUI[]): void {
489
        generateExpressionsList(expressions, operator, expressionsUIs);
5,247✔
490
    }
491

492
    public isFilteringExpressionsTreeEmpty(expressionTree: IFilteringExpressionsTree): boolean {
493
        if (FilteringExpressionsTree.empty(expressionTree)) {
1,520✔
494
            return true;
1,022✔
495
        }
496

497
        for (const expr of expressionTree.filteringOperands) {
498✔
498
            if (isTree(expr)) {
498✔
499
                if (expr.filteringOperands && expr.filteringOperands.length) {
450✔
500
                    return false;
450✔
501
                }
502
            } else {
503
                return false;
48✔
504
            }
505
        }
506
        return true;
×
507
    }
508

509
    protected filter_internal(fieldName: string, term, conditionOrExpressionsTree: IFilteringOperation | IFilteringExpressionsTree,
510
        ignoreCase: boolean) {
511
        const filteringTree = this.grid.filteringExpressionsTree;
393✔
512
        this.grid.crudService.endEdit(false);
393✔
513
        this.grid.page = 0;
393✔
514

515
        const fieldFilterIndex = ExpressionsTreeUtil.findIndex(filteringTree, fieldName);
393✔
516
        this.prepare_filtering_expression(filteringTree, fieldName, term, conditionOrExpressionsTree, ignoreCase, fieldFilterIndex);
393✔
517
        this.grid.filteringExpressionsTree = filteringTree;
393✔
518
    }
519

520
    /** Modifies the filteringState object to contain the newly added filtering conditions/expressions.
521
     * If createNewTree is true, filteringState will not be modified (because it directly affects the grid.filteringExpressionsTree),
522
     * but a new object is created and returned.
523
     */
524
    protected prepare_filtering_expression(
525
        filteringState: IFilteringExpressionsTree,
526
        fieldName: string,
527
        searchVal,
528
        conditionOrExpressionsTree: IFilteringOperation | IFilteringExpressionsTree,
529
        ignoreCase: boolean,
530
        insertAtIndex = -1,
16✔
531
        createNewTree = false): FilteringExpressionsTree {
415✔
532

533
        let expressionsTree = conditionOrExpressionsTree && 'operator' in conditionOrExpressionsTree ?
809✔
534
            conditionOrExpressionsTree : null;
535
        const condition = conditionOrExpressionsTree && 'operator' in conditionOrExpressionsTree ?
809✔
536
            null : conditionOrExpressionsTree as IFilteringOperation;
537

538
        let newExpressionsTree = filteringState as FilteringExpressionsTree;
809✔
539

540
        if (createNewTree) {
809✔
541
            newExpressionsTree = new FilteringExpressionsTree(filteringState.operator, filteringState.fieldName);
394✔
542
            newExpressionsTree.filteringOperands = [...filteringState.filteringOperands];
394✔
543
        }
544

545
        if (condition) {
809✔
546
            const newExpression: IFilteringExpression = { fieldName: fieldName, searchVal, condition, conditionName: condition.name, ignoreCase };
536✔
547
            expressionsTree = new FilteringExpressionsTree(filteringState.operator, fieldName);
536✔
548
            expressionsTree.filteringOperands.push(newExpression);
536✔
549
        }
550

551
        if (expressionsTree) {
809✔
552
            if (insertAtIndex > -1) {
808✔
553
                newExpressionsTree.filteringOperands[insertAtIndex] = expressionsTree;
92✔
554
            } else {
555
                newExpressionsTree.filteringOperands.push(expressionsTree);
716✔
556
            }
557
        }
558

559
        return newExpressionsTree;
809✔
560
    }
561

562

563
    private isFilteringTreeComplex(expressions: IFilteringExpressionsTree | IFilteringExpression): boolean {
564
        if (!expressions) {
4,332✔
565
            return false;
1,602✔
566
        }
567

568
        if (isTree(expressions)) {
2,730✔
569
            if (expressions.operator === FilteringLogic.Or) {
1,317✔
570
                const andOperatorsCount = this.getChildAndOperatorsCount(expressions);
418✔
571

572
                // having more than one 'And' operator in the sub-tree means that the filter could not be represented without parentheses.
573
                return andOperatorsCount > 1;
418✔
574
            }
575

576
            let isComplex = false;
899✔
577
            for (const operand of expressions.filteringOperands) {
899✔
578
                isComplex = isComplex || this.isFilteringTreeComplex(operand);
1,413✔
579
            }
580

581
            return isComplex;
899✔
582
        }
583

584
        return false;
1,413✔
585
    }
586

587
    private getChildAndOperatorsCount(expressions: IFilteringExpressionsTree): number {
588
        let count = 0;
418✔
589
        let operand;
590
        for (let i = 0; i < expressions.filteringOperands.length; i++) {
418✔
591
            operand = expressions[i];
449✔
592
            if (operand && isTree(operand)) {
449!
593
                if (operand.operator === FilteringLogic.And) {
×
594
                    count++;
×
595
                }
596

NEW
597
                count = count + this.getChildAndOperatorsCount(operand as IFilteringExpressionsTree);
×
598
            }
599
        }
600

601
        return count;
418✔
602
    }
603
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc