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

IgniteUI / igniteui-angular / 26023601418

18 May 2026 08:57AM UTC coverage: 4.854% (-85.3%) from 90.174%
26023601418

Pull #17281

github

web-flow
Merge e7ce7a18e into 5a85df190
Pull Request #17281: feat: Added virtual scroll component and sample implementation

400 of 17347 branches covered (2.31%)

Branch coverage included in aggregate %.

63 of 222 new or added lines in 4 files covered. (28.38%)

27932 existing lines in 341 files now uncovered.

2022 of 32547 relevant lines covered (6.21%)

0.72 hits per line

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

0.69
/projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.ts
1
import { AfterViewInit, EventEmitter, LOCALE_ID, Output, TemplateRef, inject } from '@angular/core';
2
import { NgTemplateOutlet, NgClass } from '@angular/common';
3

4
import {
5
    Component, Input, ViewChild, ChangeDetectorRef, ViewChildren, QueryList, ElementRef, OnDestroy, HostBinding
6
} from '@angular/core';
7
import { FormsModule } from '@angular/forms';
8
import { Subject } from 'rxjs';
9
import { IgxChipComponent } from 'igniteui-angular/chips';
10
import {
11
    IQueryBuilderResourceStrings,
12
    QueryBuilderResourceStringsEN,
13
    PlatformUtil,
14
    trackByIdentity,
15
    GridColumnDataType,
16
    DataUtil,
17
    IgxBooleanFilteringOperand,
18
    IgxDateFilteringOperand,
19
    IgxDateTimeFilteringOperand,
20
    IgxNumberFilteringOperand,
21
    IgxStringFilteringOperand,
22
    IgxTimeFilteringOperand,
23
    FilteringLogic,
24
    IFilteringExpression,
25
    FilteringExpressionsTree,
26
    IExpressionTree,
27
    IFilteringExpressionsTree,
28
    FieldType,
29
    EntityType,
30
    HorizontalAlignment,
31
    OverlaySettings,
32
    VerticalAlignment,
33
    AbsoluteScrollStrategy,
34
    AutoPositionStrategy,
35
    ConnectedPositioningStrategy,
36
    IgxPickerToggleComponent,
37
    IgxPickerClearComponent,
38
    getCurrentResourceStrings,
39
    DEFAULT_LOCALE,
40
    onResourceChangeHandle,
41
    IgxDateFormatterPipe,
42
    isTree
43
} from 'igniteui-angular/core';
44
import { IgxDatePickerComponent } from 'igniteui-angular/date-picker';
45

46
import {
47
    IgxButtonDirective,
48
    IgxDateTimeEditorDirective,
49
    IgxIconButtonDirective,
50
    IgxTooltipDirective,
51
    IgxTooltipTargetDirective,
52
    IgxDragIgnoreDirective,
53
    IgxDropDirective
54
} from 'igniteui-angular/directives';
55
import { IgxSelectComponent } from 'igniteui-angular/select';
56
import { IgxTimePickerComponent } from 'igniteui-angular/time-picker';
57
import { IgxInputGroupComponent, IgxInputDirective, IgxPrefixDirective } from 'igniteui-angular/input-group';
58
import { IgxSelectItemComponent } from 'igniteui-angular/select';
59
import { IgxIconComponent } from 'igniteui-angular/icon';
60
import { IComboSelectionChangingEventArgs, IgxComboComponent, IgxComboHeaderDirective } from 'igniteui-angular/combo';
61
import { IgxCheckboxComponent, IChangeCheckboxEventArgs } from 'igniteui-angular/checkbox';
62
import { IgxDialogComponent } from 'igniteui-angular/dialog';
63
import {
64
    ISelectionEventArgs,
65
    IgxDropDownComponent,
66
    IgxDropDownItemComponent,
67
    IgxDropDownItemNavigationDirective
68
} from 'igniteui-angular/drop-down';
69
import { IgxQueryBuilderSearchValueTemplateDirective } from './query-builder.directives';
70
import { IgxQueryBuilderComponent } from './query-builder.component';
71
import { IgxQueryBuilderDragService } from './query-builder-drag.service';
72
import { ExpressionGroupItem, ExpressionItem, ExpressionOperandItem, IgxFieldFormatterPipe, IgxQueryBuilderSearchValueContext } from './query-builder.common';
73
import { getCurrentI18n, IResourceChangeEventArgs } from 'igniteui-i18n-core';
74

75
const DEFAULT_PIPE_DATE_FORMAT = 'mediumDate';
3✔
76
const DEFAULT_PIPE_TIME_FORMAT = 'mediumTime';
3✔
77
const DEFAULT_PIPE_DATE_TIME_FORMAT = 'medium';
3✔
78
const DEFAULT_PIPE_DIGITS_INFO = '1.0-3';
3✔
79
const DEFAULT_CHIP_FOCUS_DELAY = 50;
3✔
80

81
/** @hidden */
82
@Component({
83
    selector: 'igx-query-builder-tree',
84
    templateUrl: './query-builder-tree.component.html',
85
    host: { 'class': 'igx-query-builder-tree' },
86
    imports: [
87
        IgxDateFormatterPipe,
88
        FormsModule,
89
        IgxButtonDirective,
90
        IgxCheckboxComponent,
91
        IgxChipComponent,
92
        IgxComboComponent,
93
        IgxComboHeaderDirective,
94
        IgxDatePickerComponent,
95
        IgxDateTimeEditorDirective,
96
        IgxDialogComponent,
97
        IgxDragIgnoreDirective,
98
        IgxDropDirective,
99
        IgxDropDownComponent,
100
        IgxDropDownItemComponent,
101
        IgxDropDownItemNavigationDirective,
102
        IgxFieldFormatterPipe,
103
        IgxIconButtonDirective,
104
        IgxIconComponent,
105
        IgxInputDirective,
106
        IgxInputGroupComponent,
107
        IgxPickerClearComponent,
108
        IgxPickerToggleComponent,
109
        IgxPrefixDirective,
110
        IgxSelectComponent,
111
        IgxSelectItemComponent,
112
        IgxTimePickerComponent,
113
        IgxTooltipDirective,
114
        IgxTooltipTargetDirective,
115
        NgClass,
116
        NgTemplateOutlet
117
    ],
118
    providers: [
119
        IgxQueryBuilderDragService
120
    ],
121
})
122
export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
3✔
UNCOV
123
    public cdr = inject(ChangeDetectorRef);
×
UNCOV
124
    public dragService = inject(IgxQueryBuilderDragService);
×
UNCOV
125
    protected platform = inject(PlatformUtil);
×
UNCOV
126
    private elRef = inject(ElementRef);
×
UNCOV
127
    protected _localeId = inject(LOCALE_ID);
×
128

129
    /**
130
     * @hidden @internal
131
     */
132
    public _expressionTree: IExpressionTree;
133

134
    /**
135
     * @hidden @internal
136
     */
137
    public _expressionTreeCopy: IExpressionTree;
138

139
    /**
140
     * @hidden @internal
141
     */
142
    @HostBinding('class') public get getClass() {
UNCOV
143
        return `igx-query-builder-tree--level-${this.level}`;
×
144
    }
145

146
    /**
147
     * Sets/gets the entities.
148
     */
149
    @Input()
150
    public entities: EntityType[];
151

152
    /**
153
     * Sets/gets the parent query builder component.
154
     */
155
    @Input()
156
    public queryBuilder: IgxQueryBuilderComponent;
157

158
    /**
159
     * Sets/gets the search value template.
160
     */
161
    @Input()
UNCOV
162
    public searchValueTemplate: TemplateRef<IgxQueryBuilderSearchValueContext> = null;
×
163

164
    /**
165
    * Returns the parent expression operand.
166
    */
167
    @Input()
168
    public get parentExpression(): ExpressionOperandItem {
UNCOV
169
        return this._parentExpression;
×
170
    }
171

172
    /**
173
     * Sets the parent expression operand.
174
     */
175
    public set parentExpression(value: ExpressionOperandItem) {
UNCOV
176
        this._parentExpression = value;
×
177
    }
178

179
    /**
180
    * Returns the fields.
181
    */
182
    public get fields(): FieldType[] {
UNCOV
183
        if (!this._fields && this.isAdvancedFiltering()) {
×
UNCOV
184
            this._fields = this.entities[0].fields;
×
185
        }
186

UNCOV
187
        return this._fields;
×
188
    }
189

190
    /**
191
     * Sets the fields.
192
     */
193
    @Input()
194
    public set fields(fields: FieldType[]) {
UNCOV
195
        this._fields = fields;
×
196

UNCOV
197
        this._fields = this._fields?.map(f => ({...f, filters: this.getFilters(f), pipeArgs: this.getPipeArgs(f) }));
×
198

UNCOV
199
        if (!this._fields && this.isAdvancedFiltering()) {
×
UNCOV
200
            this._fields = this.entities[0].fields;
×
201
        }
202
    }
203

204
    /**
205
    * Returns the expression tree.
206
    */
207
    public get expressionTree(): IExpressionTree {
UNCOV
208
        return this._expressionTree;
×
209
    }
210

211
    /**
212
     * Sets the expression tree.
213
     */
214
    @Input()
215
    public set expressionTree(expressionTree: IExpressionTree) {
UNCOV
216
        this._expressionTree = expressionTree;
×
UNCOV
217
        if (!expressionTree) {
×
UNCOV
218
            this._selectedEntity = this.isAdvancedFiltering() && this.entities.length === 1 ? this.entities[0] : null;
×
UNCOV
219
            this._selectedReturnFields = this._selectedEntity ? this._selectedEntity.fields?.map(f => f.field) : [];
×
220
        }
221

UNCOV
222
        if (!this._preventInit) {
×
UNCOV
223
            this.init();
×
224
        }
225
    }
226

227
    /**
228
     * Gets the `locale` of the query builder.
229
     * If not set, defaults to application's locale.
230
     */
231
    @Input()
232
    public get locale(): string {
UNCOV
233
        return this._locale || this._defaultLocale;
×
234
    }
235

236
    /**
237
     * Sets the `locale` of the query builder.
238
     * Expects a valid BCP 47 language tag.
239
     */
240
    public set locale(value: string) {
UNCOV
241
        this._locale = value;
×
UNCOV
242
        this._defaultResourceStrings = getCurrentResourceStrings(QueryBuilderResourceStringsEN, false, this._locale);
×
243
    }
244

245
    /**
246
     * Sets the resource strings.
247
     * By default it uses EN resources.
248
     */
249
    @Input()
250
    public set resourceStrings(value: IQueryBuilderResourceStrings) {
UNCOV
251
        this._resourceStrings = Object.assign({}, this._resourceStrings, value);
×
252
    }
253

254
    /**
255
     * Returns the resource strings.
256
     */
257
    public get resourceStrings(): IQueryBuilderResourceStrings {
UNCOV
258
        return this._resourceStrings || this._defaultResourceStrings;
×
259
    }
260

261
    /**
262
     * Gets/sets the expected return field.
263
     */
UNCOV
264
    @Input() public expectedReturnField: string = null;
×
265

266
    /**
267
     * Event fired as the expression tree is changed.
268
     */
269
    @Output()
UNCOV
270
    public expressionTreeChange = new EventEmitter<IExpressionTree>();
×
271

272
    /**
273
     * Event fired if a nested query builder tree is being edited.
274
     */
275
    @Output()
UNCOV
276
    public inEditModeChange = new EventEmitter<ExpressionOperandItem>();
×
277

278
    @ViewChild('entitySelect', { read: IgxSelectComponent })
279
    protected entitySelect: IgxSelectComponent;
280

281
    @ViewChild('editingInputs', { read: ElementRef })
282
    private editingInputs: ElementRef;
283

284
    @ViewChild('returnFieldsCombo', { read: IgxComboComponent })
285
    private returnFieldsCombo: IgxComboComponent;
286

287
    @ViewChild('returnFieldSelect', { read: IgxSelectComponent })
288
    protected returnFieldSelect: IgxSelectComponent;
289

290
    @ViewChild('fieldSelect', { read: IgxSelectComponent })
291
    private fieldSelect: IgxSelectComponent;
292

293
    @ViewChild('conditionSelect', { read: IgxSelectComponent })
294
    private conditionSelect: IgxSelectComponent;
295

296
    @ViewChild('searchValueInput', { read: ElementRef })
297
    private searchValueInput: ElementRef;
298

299
    @ViewChild('picker')
300
    private picker: IgxDatePickerComponent | IgxTimePickerComponent;
301

302
    @ViewChild('addRootAndGroupButton', { read: ElementRef })
303
    private addRootAndGroupButton: ElementRef;
304

305
    @ViewChild('addConditionButton', { read: ElementRef })
306
    private addConditionButton: ElementRef;
307

308
    @ViewChild('entityChangeDialog', { read: IgxDialogComponent })
309
    private entityChangeDialog: IgxDialogComponent;
310

311
    @ViewChild('addOptionsDropDown', { read: IgxDropDownComponent })
312
    private addExpressionItemDropDown: IgxDropDownComponent;
313

314
    @ViewChild('groupContextMenuDropDown', { read: IgxDropDownComponent })
315
    private groupContextMenuDropDown: IgxDropDownComponent;
316

317
    /**
318
     * @hidden @internal
319
     */
320
    @ViewChildren(IgxChipComponent, { read: IgxChipComponent })
321
    public expressionsChips: QueryList<IgxChipComponent>;
322

323
    @ViewChild('editingInputsContainer', { read: ElementRef })
324
    protected set editingInputsContainer(value: ElementRef) {
UNCOV
325
        if ((value && !this._editingInputsContainer) ||
×
326
            (value && this._editingInputsContainer && this._editingInputsContainer.nativeElement !== value.nativeElement)) {
UNCOV
327
            requestAnimationFrame(() => {
×
UNCOV
328
                this.scrollElementIntoView(value.nativeElement);
×
329
            });
330
        }
331

UNCOV
332
        this._editingInputsContainer = value;
×
333
    }
334

335
    /** @hidden */
336
    protected get editingInputsContainer(): ElementRef {
337
        return this._editingInputsContainer;
×
338
    }
339

340
    @ViewChild('currentGroupButtonsContainer', { read: ElementRef })
341
    protected set currentGroupButtonsContainer(value: ElementRef) {
UNCOV
342
        if ((value && !this._currentGroupButtonsContainer) ||
×
343
            (value && this._currentGroupButtonsContainer && this._currentGroupButtonsContainer.nativeElement !== value.nativeElement)) {
UNCOV
344
            requestAnimationFrame(() => {
×
UNCOV
345
                this.scrollElementIntoView(value.nativeElement);
×
346
            });
347
        }
348

UNCOV
349
        this._currentGroupButtonsContainer = value;
×
350
    }
351

352
    /** @hidden */
353
    protected get currentGroupButtonsContainer(): ElementRef {
354
        return this._currentGroupButtonsContainer;
×
355
    }
356

357
    @ViewChild('expressionsContainer')
358
    private expressionsContainer: ElementRef;
359

360
    @ViewChildren(IgxQueryBuilderTreeComponent)
361
    private innerQueries: QueryList<IgxQueryBuilderTreeComponent>;
362

363
    /**
364
     * @hidden @internal
365
     */
366
    public innerQueryNewExpressionTree: IExpressionTree;
367

368
    /**
369
     * @hidden @internal
370
     */
371
    public rootGroup: ExpressionGroupItem;
372

373
    /**
374
     * @hidden @internal
375
     */
UNCOV
376
    public selectedExpressions: ExpressionOperandItem[] = [];
×
377

378
    /**
379
     * @hidden @internal
380
     */
381
    public currentGroup: ExpressionGroupItem;
382

383
    /**
384
     * @hidden @internal
385
     */
386
    public contextualGroup: ExpressionGroupItem;
387

388
    /**
389
     * @hidden @internal
390
     */
391
    public filteringLogics;
392

393
    /**
394
     * @hidden @internal
395
     */
396
    public selectedCondition: string;
397

398
    /**
399
     * @hidden @internal
400
     */
UNCOV
401
    public searchValue: { value: any } = { value: null };
×
402

403
    /**
404
     * @hidden @internal
405
     */
406
    public prevFocusedExpression: ExpressionOperandItem;
407

408
    /**
409
     * @hidden @internal
410
     */
UNCOV
411
    public initialOperator = 0;
×
412

413
    /**
414
     * @hidden @internal
415
     */
UNCOV
416
    public returnFieldSelectOverlaySettings: OverlaySettings = {
×
417
        scrollStrategy: new AbsoluteScrollStrategy(),
418
        modal: false,
419
        closeOnOutsideClick: true
420
    };
421

422
    /**
423
     * @hidden @internal
424
     */
UNCOV
425
    public entitySelectOverlaySettings: OverlaySettings = {
×
426
        scrollStrategy: new AbsoluteScrollStrategy(),
427
        modal: false,
428
        closeOnOutsideClick: true
429
    };
430

431
    /**
432
     * @hidden @internal
433
     */
UNCOV
434
    public fieldSelectOverlaySettings: OverlaySettings = {
×
435
        scrollStrategy: new AbsoluteScrollStrategy(),
436
        modal: false,
437
        closeOnOutsideClick: true
438
    };
439

440
    /**
441
     * @hidden @internal
442
     */
UNCOV
443
    public conditionSelectOverlaySettings: OverlaySettings = {
×
444
        scrollStrategy: new AbsoluteScrollStrategy(),
445
        modal: false,
446
        closeOnOutsideClick: true
447
    };
448

449
    /**
450
     * @hidden @internal
451
     */
UNCOV
452
    public addExpressionDropDownOverlaySettings: OverlaySettings = {
×
453
        scrollStrategy: new AbsoluteScrollStrategy(),
454
        modal: false,
455
        closeOnOutsideClick: true
456
    };
457

458
    /**
459
     * @hidden @internal
460
     */
UNCOV
461
    public groupContextMenuDropDownOverlaySettings: OverlaySettings = {
×
462
        scrollStrategy: new AbsoluteScrollStrategy(),
463
        modal: false,
464
        closeOnOutsideClick: true
465
    };
466

UNCOV
467
    private destroy$ = new Subject<any>();
×
468
    private _timeoutId: any;
469
    private _lastFocusedChipIndex: number;
UNCOV
470
    private _focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
×
471
    private _parentExpression: ExpressionOperandItem;
472
    private _selectedEntity: EntityType;
473
    private _selectedReturnFields: string | string[];
474
    private _selectedField: FieldType;
475
    private _editingInputsContainer: ElementRef;
476
    private _currentGroupButtonsContainer: ElementRef;
477
    private _addModeExpression: ExpressionOperandItem;
478
    private _editedExpression: ExpressionOperandItem;
UNCOV
479
    private _preventInit = false;
×
480
    private _prevFocusedContainer: ElementRef;
UNCOV
481
    private _expandedExpressions: IFilteringExpression[] = [];
×
482
    private _fields: FieldType[];
483
    private _locale;
484
    private _defaultLocale;
485
    private _entityNewValue: EntityType;
UNCOV
486
    private _resourceStrings = null;
×
UNCOV
487
    private _defaultResourceStrings = getCurrentResourceStrings(QueryBuilderResourceStringsEN);
×
488

489
    /**
490
     * Returns if the select entity dropdown at the root level is disabled after the initial selection.
491
     */
492
    public get disableEntityChange(): boolean {
493

UNCOV
494
        return !this.parentExpression && this.selectedEntity ? this.queryBuilder.disableEntityChange : false;
×
495
    }
496

497
    /**
498
     * Returns if the fields combo at the root level is disabled.
499
     */
500
    public get disableReturnFieldsChange(): boolean {
501

UNCOV
502
        return !this.selectedEntity || this.queryBuilder.disableReturnFieldsChange;
×
503
    }
504

505
    /**
506
     * Returns the current level.
507
     */
508
    public get level(): number {
UNCOV
509
        let parent = this.elRef.nativeElement.parentElement;
×
UNCOV
510
        let _level = 0;
×
UNCOV
511
        while (parent) {
×
UNCOV
512
            if (parent.localName === 'igx-query-builder-tree') {
×
UNCOV
513
                _level++;
×
514
            }
UNCOV
515
            parent = parent.parentElement;
×
516
        }
UNCOV
517
        return _level;
×
518
    }
519

520
    /** @hidden */
521
    protected isAdvancedFiltering(): boolean {
UNCOV
522
        return (this.entities?.length === 1 && !this.entities[0]?.name) ||
×
UNCOV
523
            this.entities?.find(e => e.childEntities?.length > 0) !== undefined ||
×
524
            (this.entities?.length > 0 && this.queryBuilder?.entities?.length > 0 && this.entities !== this.queryBuilder?.entities);
525
    }
526

527
    /** @hidden */
528
    protected isHierarchicalNestedQuery(): boolean {
UNCOV
529
        return this.queryBuilder.entities !== this.entities
×
530
    }
531

532
    /** @hidden */
533
    protected isSearchValueInputDisabled(): boolean {
UNCOV
534
        return !this.selectedField ||
×
535
            !this.selectedCondition ||
536
            (this.selectedField &&
537
                (this.selectedField.filters.condition(this.selectedCondition).isUnary ||
538
                    this.selectedField.filters.condition(this.selectedCondition).isNestedQuery));
539
    }
540

541
    constructor() {
UNCOV
542
        this.initLocale();
×
UNCOV
543
        this.dragService.register(this, this.elRef);
×
544
    }
545

546
    /**
547
     * @hidden @internal
548
     */
549
    public ngAfterViewInit(): void {
UNCOV
550
        if (this.isAdvancedFiltering() && this.entities?.length === 1) {
×
UNCOV
551
            this.selectedEntity = this.entities[0].name;
×
UNCOV
552
            if (this._selectedEntity.fields.find(f => f.field === this.expectedReturnField)) {
×
UNCOV
553
                this._selectedReturnFields = [this.expectedReturnField];
×
554
            }
555
        }
556

557
        // Trigger additional change detection cycle
UNCOV
558
        this.cdr.detectChanges();
×
559
    }
560

561
    /**
562
     * @hidden @internal
563
     */
564
    public ngOnDestroy(): void {
UNCOV
565
        this.destroy$.next(true);
×
UNCOV
566
        this.destroy$.complete();
×
567
    }
568

569
    /**
570
     * @hidden @internal
571
     */
572
    public set selectedEntity(value: string) {
UNCOV
573
        this._selectedEntity = this.entities?.find(el => el.name === value);
×
574
    }
575

576
    /**
577
     * @hidden @internal
578
     */
579
    public get selectedEntity(): EntityType {
UNCOV
580
        return this._selectedEntity;
×
581
    }
582

583
    /**
584
     * @hidden @internal
585
     */
586
    public onEntitySelectChanging(event: ISelectionEventArgs) {
UNCOV
587
        event.cancel = true;
×
UNCOV
588
        this._entityNewValue = event.newSelection.value;
×
UNCOV
589
        if (event.oldSelection.value && this.queryBuilder.showEntityChangeDialog) {
×
UNCOV
590
            this.entityChangeDialog.open();
×
591
        } else {
UNCOV
592
            this.onEntityChangeConfirm();
×
593
        }
594
    }
595

596
    /**
597
     * @hidden
598
     */
599
    public onShowEntityChangeDialogChange(eventArgs: IChangeCheckboxEventArgs) {
UNCOV
600
        this.queryBuilder.showEntityChangeDialog = !eventArgs.checked;
×
601
    }
602

603
    /**
604
     * @hidden
605
     */
606
    public onEntityChangeCancel() {
UNCOV
607
        this.entityChangeDialog.close();
×
UNCOV
608
        this.entitySelect.close();
×
UNCOV
609
        this._entityNewValue = null;
×
610
    }
611

612
    /**
613
     * @hidden
614
     */
615
    public onEntityChangeConfirm() {
UNCOV
616
        if (this._parentExpression) {
×
UNCOV
617
            this._expressionTree = this.createExpressionTreeFromGroupItem(this.createExpressionGroupItem(this._expressionTree));
×
618
        }
619

UNCOV
620
        this._selectedEntity = this._entityNewValue;
×
UNCOV
621
        if (!this._selectedEntity.fields) {
×
622
            this._selectedEntity.fields = [];
×
623
        }
UNCOV
624
        this.fields = this._entityNewValue ? this._entityNewValue.fields : [];
×
625

UNCOV
626
        if (this._selectedEntity.fields.find(f => f.field === this.expectedReturnField)) {
×
UNCOV
627
            this._selectedReturnFields = [this.expectedReturnField];
×
628
        } else {
UNCOV
629
            this._selectedReturnFields = this.parentExpression ? [] : this._entityNewValue.fields?.map(f => f.field);
×
630
        }
631

UNCOV
632
        if (this._expressionTree) {
×
UNCOV
633
            this._expressionTree.entity = this._entityNewValue.name;
×
634

UNCOV
635
            const returnFields = Array.isArray(this._selectedReturnFields) ? this._selectedReturnFields : [this._selectedReturnFields];
×
UNCOV
636
            this._expressionTree.returnFields = this.fields.length === returnFields.length ? ['*'] : returnFields;
×
637

UNCOV
638
            this._expressionTree.filteringOperands = [];
×
639

UNCOV
640
            this._editedExpression = null;
×
UNCOV
641
            if (!this.parentExpression) {
×
UNCOV
642
                this.expressionTreeChange.emit(this._expressionTree);
×
643
            }
644

UNCOV
645
            this.rootGroup = null;
×
UNCOV
646
            this.currentGroup = this.rootGroup;
×
647
        }
648

UNCOV
649
        this._selectedField = null;
×
UNCOV
650
        this.selectedCondition = null;
×
UNCOV
651
        this.searchValue.value = null;
×
652

UNCOV
653
        this.entityChangeDialog.close();
×
UNCOV
654
        this.entitySelect.close();
×
655

UNCOV
656
        this._entityNewValue = null;
×
UNCOV
657
        this.innerQueryNewExpressionTree = null;
×
658

UNCOV
659
        this.initExpressionTree(this._selectedEntity.name, this.selectedReturnFields);
×
660
    }
661

662
    /**
663
     * @hidden @internal
664
     */
665
    public set selectedReturnFields(value: string[]) {
UNCOV
666
        if (this._selectedReturnFields !== value) {
×
UNCOV
667
            this._selectedReturnFields = value;
×
668

UNCOV
669
            if (this._expressionTree && !this.parentExpression) {
×
UNCOV
670
                this._expressionTree.returnFields = value.length === this.fields.length ? ['*'] : value;
×
UNCOV
671
                this.expressionTreeChange.emit(this._expressionTree);
×
672
            }
673
        }
674
    }
675

676
    /**
677
     * @hidden @internal
678
     */
679
    public get selectedReturnFields(): string[] {
UNCOV
680
        if (typeof this._selectedReturnFields == 'string') {
×
681
            return [this._selectedReturnFields];
×
682
        }
UNCOV
683
        return this._selectedReturnFields;
×
684
    }
685

686
    /**
687
     * @hidden @internal
688
     */
689
    public set selectedField(value: FieldType) {
UNCOV
690
        const oldValue = this._selectedField;
×
691

UNCOV
692
        if (this._selectedField !== value) {
×
UNCOV
693
            this._selectedField = value;
×
UNCOV
694
            if (this._selectedField && !this._selectedField.dataType) {
×
695
                this._selectedField.filters = this.getFilters(this._selectedField);
×
696
            }
697

UNCOV
698
            this.selectDefaultCondition();
×
UNCOV
699
            if (oldValue && this._selectedField && this._selectedField.dataType !== oldValue.dataType) {
×
UNCOV
700
                this.searchValue.value = null;
×
UNCOV
701
                this.cdr.detectChanges();
×
702
            }
703
        }
704
    }
705

706
    /**
707
     * @hidden @internal
708
     */
709
    public get selectedField(): FieldType {
UNCOV
710
        return this._selectedField;
×
711
    }
712

713
    /**
714
     * @hidden @internal
715
     *
716
     * used by the grid
717
     */
718
    public get isContextMenuVisible(): boolean {
719
        return !this.groupContextMenuDropDown.collapsed;
×
720
    }
721

722
    /**
723
     * @hidden @internal
724
     */
725
    public get hasEditedExpression(): boolean {
UNCOV
726
        return this._editedExpression !== undefined && this._editedExpression !== null;
×
727
    }
728

729
    /**
730
     * @hidden @internal
731
     */
732
    public addCondition(parent: ExpressionGroupItem, afterExpression?: ExpressionOperandItem, isUIInteraction?: boolean) {
UNCOV
733
        this.cancelOperandAdd();
×
734

UNCOV
735
        const operandItem = new ExpressionOperandItem({
×
736
            fieldName: null,
737
            condition: null,
738
            conditionName: null,
739
            ignoreCase: true,
740
            searchVal: null
741
        }, parent);
742

UNCOV
743
        const groupItem = new ExpressionGroupItem(this.getOperator(null) ?? FilteringLogic.And, parent);
×
UNCOV
744
        this.contextualGroup = groupItem;
×
UNCOV
745
        this.initialOperator = null;
×
746

UNCOV
747
        this._lastFocusedChipIndex = this._lastFocusedChipIndex === undefined ? -1 : this._lastFocusedChipIndex;
×
748

UNCOV
749
        if (parent) {
×
UNCOV
750
            if (afterExpression) {
×
UNCOV
751
                const index = parent.children.indexOf(afterExpression);
×
UNCOV
752
                parent.children.splice(index + 1, 0, operandItem);
×
753
            } else {
UNCOV
754
                parent.children.push(operandItem);
×
755
            }
UNCOV
756
            this._lastFocusedChipIndex++;
×
757
        } else {
UNCOV
758
            this.rootGroup = groupItem;
×
UNCOV
759
            operandItem.parent = groupItem;
×
UNCOV
760
            this.rootGroup.children.push(operandItem);
×
UNCOV
761
            this._lastFocusedChipIndex = 0;
×
762
        }
763

UNCOV
764
        this._focusDelay = 250;
×
765

UNCOV
766
        if (isUIInteraction && !afterExpression) {
×
UNCOV
767
            this._lastFocusedChipIndex = this.expressionsChips.length;
×
UNCOV
768
            this._focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
×
769
        }
770

UNCOV
771
        this.enterExpressionEdit(operandItem);
×
772
    }
773

774
    /**
775
     * @hidden @internal
776
     */
777
    public addReverseGroup(parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
UNCOV
778
        parent = parent ?? this.rootGroup;
×
779

UNCOV
780
        if (parent.operator === FilteringLogic.And) {
×
UNCOV
781
            this.addGroup(FilteringLogic.Or, parent, afterExpression);
×
782
        } else {
UNCOV
783
            this.addGroup(FilteringLogic.And, parent, afterExpression);
×
784
        }
785
    }
786

787
    /**
788
     * @hidden @internal
789
     */
790
    public endGroup(groupItem: ExpressionGroupItem) {
791
        this.currentGroup = groupItem.parent;
×
792
    }
793

794
    /**
795
     * @hidden @internal
796
     */
797
    public commitExpression() {
UNCOV
798
        this.commitOperandEdit();
×
UNCOV
799
        this.focusEditedExpressionChip();
×
800
    }
801

802
    /**
803
     * @hidden @internal
804
     */
805
    public discardExpression(expressionItem?: ExpressionOperandItem) {
UNCOV
806
        this.cancelOperandEdit();
×
UNCOV
807
        if (expressionItem && expressionItem.expression.fieldName) {
×
UNCOV
808
            this.focusEditedExpressionChip();
×
809
        }
810
    }
811

812
    /**
813
     * @hidden @internal
814
     */
815
    public commitOperandEdit() {
UNCOV
816
        const actualSearchValue = this.searchValue.value;
×
UNCOV
817
        if (this._editedExpression) {
×
UNCOV
818
            this._editedExpression.expression.fieldName = this.selectedField.field;
×
UNCOV
819
            this._editedExpression.expression.condition = this.selectedField.filters.condition(this.selectedCondition);
×
UNCOV
820
            this._editedExpression.expression.conditionName = this.selectedCondition;
×
UNCOV
821
            this._editedExpression.expression.searchVal = DataUtil.parseValue(this.selectedField.dataType, actualSearchValue) || actualSearchValue;
×
UNCOV
822
            this._editedExpression.fieldLabel = this.selectedField.label
×
823
                ? this.selectedField.label
824
                : this.selectedField.header
×
825
                    ? this.selectedField.header
826
                    : this.selectedField.field;
827

UNCOV
828
            const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0]
×
UNCOV
829
            if (innerQuery && this.selectedField?.filters?.condition(this.selectedCondition)?.isNestedQuery) {
×
UNCOV
830
                innerQuery.exitEditAddMode();
×
UNCOV
831
                this._editedExpression.expression.searchTree = this.getExpressionTreeCopy(innerQuery.expressionTree);
×
UNCOV
832
                const returnFields = innerQuery.selectedReturnFields.length > 0 ?
×
833
                                        innerQuery.selectedReturnFields :
834
                                        [innerQuery.fields[0].field];
UNCOV
835
                this._editedExpression.expression.searchTree.returnFields = returnFields;
×
836
            } else {
UNCOV
837
                this._editedExpression.expression.searchTree = null;
×
838
            }
UNCOV
839
            this.innerQueryNewExpressionTree = null;
×
840

UNCOV
841
            if (this.selectedField.filters.condition(this.selectedCondition)?.isUnary || this.selectedField.filters.condition(this.selectedCondition)?.isNestedQuery) {
×
UNCOV
842
                this._editedExpression.expression.searchVal = null;
×
843
            }
844

UNCOV
845
            this._editedExpression.inEditMode = false;
×
UNCOV
846
            this._editedExpression = null;
×
847
        }
848

UNCOV
849
        if (this.selectedReturnFields.length === 0) {
×
850
            this.selectedReturnFields = this.fields.map(f => f.field);
×
851
        }
852

UNCOV
853
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup, this.selectedEntity?.name, this.selectedReturnFields);
×
UNCOV
854
        if (!this.parentExpression) {
×
UNCOV
855
            this.expressionTreeChange.emit(this._expressionTree);
×
856
        }
857
    }
858

859
    /**
860
     * @hidden @internal
861
     */
862
    public cancelOperandAdd() {
UNCOV
863
        if (this._addModeExpression) {
×
864
            this._addModeExpression.inAddMode = false;
×
865
            this._addModeExpression = null;
×
866
        }
867
    }
868

869
    /**
870
     * @hidden @internal
871
     */
UNCOV
872
    public deleteItem = (expressionItem: ExpressionItem, skipEmit: boolean = false) => {
×
UNCOV
873
        if (!expressionItem.parent) {
×
UNCOV
874
            this.rootGroup = null;
×
UNCOV
875
            this.currentGroup = null;
×
876
            //this._expressionTree = null;
UNCOV
877
            return;
×
878
        }
879

UNCOV
880
        if (expressionItem === this.currentGroup) {
×
881
            this.currentGroup = this.currentGroup.parent;
×
882
        }
883

UNCOV
884
        const children = expressionItem.parent.children;
×
UNCOV
885
        const index = children.indexOf(expressionItem);
×
UNCOV
886
        children.splice(index, 1);
×
UNCOV
887
        const entity = this.expressionTree ? this.expressionTree.entity : null;
×
UNCOV
888
        const returnFields = this.expressionTree ? this.expressionTree.returnFields : null;
×
UNCOV
889
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup, entity, returnFields); // TODO: don't recreate if not necessary
×
890

UNCOV
891
        if (!children.length) {
×
UNCOV
892
            this.deleteItem(expressionItem.parent, true);
×
893
        }
894

UNCOV
895
        if (!this.parentExpression && !skipEmit) {
×
UNCOV
896
            this.expressionTreeChange.emit(this._expressionTree);
×
897
        }
898
    }
899

900
    /**
901
     * @hidden @internal
902
     */
903
    public cancelOperandEdit() {
UNCOV
904
        if (this.innerQueries) {
×
UNCOV
905
            const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
×
UNCOV
906
            if (innerQuery) {
×
UNCOV
907
                if (innerQuery._editedExpression) {
×
UNCOV
908
                    innerQuery.cancelOperandEdit();
×
909
                }
910

UNCOV
911
                innerQuery.expressionTree = this.getExpressionTreeCopy(this._editedExpression.expression.searchTree);
×
UNCOV
912
                this.innerQueryNewExpressionTree = null;
×
913
            }
914
        }
915

UNCOV
916
        if (this._editedExpression) {
×
UNCOV
917
            this._editedExpression.inEditMode = false;
×
918

UNCOV
919
            if (!this._editedExpression.expression.fieldName) {
×
UNCOV
920
                this.deleteItem(this._editedExpression);
×
921
            }
922

UNCOV
923
            this._editedExpression = null;
×
924
        }
925

UNCOV
926
        if (!this.expressionTree && this.contextualGroup) {
×
UNCOV
927
            this.initialOperator = this.contextualGroup.operator;
×
928
        }
929
    }
930

931
    /**
932
     * @hidden @internal
933
     */
934
    public operandCanBeCommitted(): boolean {
UNCOV
935
        const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
×
UNCOV
936
        return this.selectedField && this.selectedCondition &&
×
937
            (
938
                (
939
                    ((!Array.isArray(this.searchValue.value) && !!this.searchValue.value) || (Array.isArray(this.searchValue.value) && this.searchValue.value.length !== 0)) &&
940
                    !(this.selectedField?.filters?.condition(this.selectedCondition)?.isNestedQuery)
941
                ) ||
942
                (
943
                    this.selectedField?.filters?.condition(this.selectedCondition)?.isNestedQuery && innerQuery && !!innerQuery.expressionTree && innerQuery.selectedReturnFields?.length > 0
944
                ) ||
945
                this.selectedField.filters.condition(this.selectedCondition)?.isUnary
946
            );
947
    }
948

949
    /**
950
     * @hidden @internal
951
     */
952
    public canCommitCurrentState(): boolean {
UNCOV
953
        const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
×
UNCOV
954
        if (innerQuery) {
×
UNCOV
955
            return this.selectedReturnFields?.length > 0 && innerQuery.canCommitCurrentState();
×
956
        } else {
UNCOV
957
            return this.selectedReturnFields?.length > 0 &&
×
958
                (
959
                    (!this._editedExpression) || // no edited expr
960
                    (this._editedExpression && !this.selectedField) || // empty edited expr
961
                    (this._editedExpression && this.operandCanBeCommitted() === true) // valid edited expr
962
                );
963
        }
964
    }
965

966
    /**
967
     * @hidden @internal
968
     */
969
    public commitCurrentState(): void {
UNCOV
970
        const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
×
UNCOV
971
        if (innerQuery) {
×
UNCOV
972
            innerQuery.commitCurrentState();
×
973
        }
974

UNCOV
975
        if (this._editedExpression) {
×
UNCOV
976
            if (this.selectedField) {
×
UNCOV
977
                this.commitOperandEdit();
×
978
            } else {
979
                this.deleteItem(this._editedExpression);
×
980
                this._editedExpression = null;
×
981
            }
982
        }
983
    }
984

985
    /**
986
     * @hidden @internal
987
     */
988
    public exitEditAddMode(shouldPreventInit = false) {
×
UNCOV
989
        if (!this._editedExpression) {
×
UNCOV
990
            return;
×
991
        }
992

UNCOV
993
        this.exitOperandEdit();
×
UNCOV
994
        this.cancelOperandAdd();
×
995

UNCOV
996
        if (shouldPreventInit) {
×
UNCOV
997
            this._preventInit = true;
×
998
        }
999
    }
1000

1001
    /**
1002
     * @hidden @internal
1003
     *
1004
     * used by the grid
1005
     */
1006
    public exitOperandEdit() {
UNCOV
1007
        if (!this._editedExpression) {
×
UNCOV
1008
            return;
×
1009
        }
1010

UNCOV
1011
        if (this.operandCanBeCommitted()) {
×
UNCOV
1012
            this.commitOperandEdit();
×
1013
        } else {
UNCOV
1014
            this.cancelOperandEdit();
×
1015
        }
1016
    }
1017

1018
    /**
1019
     * @hidden @internal
1020
     */
1021
    public isExpressionGroup(expression: ExpressionItem): boolean {
UNCOV
1022
        return expression instanceof ExpressionGroupItem;
×
1023
    }
1024

1025
    /**
1026
     * @hidden @internal
1027
     */
1028
    public onExpressionFocus(expressionItem: ExpressionOperandItem) {
UNCOV
1029
        if (this.prevFocusedExpression) {
×
UNCOV
1030
            this.prevFocusedExpression.focused = false;
×
1031
        }
UNCOV
1032
        expressionItem.focused = true;
×
UNCOV
1033
        this.prevFocusedExpression = expressionItem;
×
1034
    }
1035

1036
    /**
1037
     * @hidden @internal
1038
     */
1039
    public onExpressionBlur(event, expressionItem: ExpressionOperandItem) {
UNCOV
1040
        if (this._prevFocusedContainer && this._prevFocusedContainer !== event.target.closest('.igx-filter-tree__expression-item')) {
×
1041
            expressionItem.focused = false;
×
1042
        }
UNCOV
1043
        this._prevFocusedContainer = event.target.closest('.igx-filter-tree__expression-item');
×
1044
    }
1045

1046
    /**
1047
     * @hidden @internal
1048
     */
1049
    public onChipRemove(expressionItem: ExpressionItem) {
UNCOV
1050
        this.exitEditAddMode();
×
UNCOV
1051
        this.deleteItem(expressionItem);
×
1052
    }
1053

1054
    /**
1055
     * @hidden @internal
1056
     */
UNCOV
1057
    public focusChipAfterDrag = (index: number) => {
×
UNCOV
1058
        this._lastFocusedChipIndex = index;
×
UNCOV
1059
        this.focusEditedExpressionChip();
×
1060
    }
1061
    /**
1062
     * @hidden @internal
1063
     */
1064
    public addExpressionBlur() {
1065
        if (this.prevFocusedExpression) {
×
1066
            this.prevFocusedExpression.focused = false;
×
1067
        }
1068
        if (this.addExpressionItemDropDown && !this.addExpressionItemDropDown.collapsed) {
×
1069
            this.addExpressionItemDropDown.close();
×
1070
        }
1071
    }
1072

1073
    /**
1074
     * @hidden @internal
1075
     */
1076
    public onChipClick(expressionItem: ExpressionOperandItem, chip: IgxChipComponent) {
UNCOV
1077
        this.enterExpressionEdit(expressionItem, chip);
×
1078
    }
1079

1080
    /**
1081
     * @hidden @internal
1082
     */
1083
    public enterExpressionEdit(expressionItem: ExpressionOperandItem, chip?: IgxChipComponent) {
UNCOV
1084
        this.exitEditAddMode(true);
×
UNCOV
1085
        this.cdr.detectChanges();
×
UNCOV
1086
        this._lastFocusedChipIndex = chip ? this.expressionsChips.toArray().findIndex(expr => expr === chip) : this._lastFocusedChipIndex;
×
UNCOV
1087
        this.enterEditMode(expressionItem);
×
1088
    }
1089

1090

1091
    /**
1092
     * @hidden @internal
1093
     */
1094
    public clickExpressionAdd(targetButton: HTMLElement, chip: IgxChipComponent) {
UNCOV
1095
        this.exitEditAddMode(true);
×
UNCOV
1096
        this.cdr.detectChanges();
×
UNCOV
1097
        this._lastFocusedChipIndex = this.expressionsChips.toArray().findIndex(expr => expr === chip);
×
UNCOV
1098
        this.openExpressionAddDialog(targetButton);
×
1099
    }
1100

1101
    /**
1102
     * @hidden @internal
1103
     */
1104
    public openExpressionAddDialog(targetButton: HTMLElement) {
UNCOV
1105
        this.addExpressionDropDownOverlaySettings.target = targetButton;
×
UNCOV
1106
        this.addExpressionDropDownOverlaySettings.positionStrategy = new ConnectedPositioningStrategy({
×
1107
            horizontalDirection: HorizontalAlignment.Right,
1108
            horizontalStartPoint: HorizontalAlignment.Left,
1109
            verticalStartPoint: VerticalAlignment.Bottom
1110
        });
1111

UNCOV
1112
        this.addExpressionItemDropDown.open(this.addExpressionDropDownOverlaySettings);
×
1113
    }
1114

1115
    /**
1116
     * @hidden @internal
1117
     */
1118
    public enterExpressionAdd(event: ISelectionEventArgs, expressionItem: ExpressionOperandItem) {
UNCOV
1119
        if (this._addModeExpression) {
×
1120
            this._addModeExpression.inAddMode = false;
×
1121
        }
1122

UNCOV
1123
        if (this.parentExpression) {
×
1124
            this.inEditModeChange.emit(this.parentExpression);
×
1125
        }
1126

UNCOV
1127
        const parent = expressionItem.parent ?? this.rootGroup;
×
UNCOV
1128
        requestAnimationFrame(() => {
×
UNCOV
1129
            if (event.newSelection.value === 'addCondition') {
×
UNCOV
1130
                this.addCondition(parent, expressionItem);
×
1131
            } else if (event.newSelection.value === 'addGroup') {
×
1132
                this.addReverseGroup(parent, expressionItem);
×
1133
            }
UNCOV
1134
            expressionItem.inAddMode = true;
×
UNCOV
1135
            this._addModeExpression = expressionItem;
×
1136
        })
1137
    }
1138

1139
    /**
1140
     * @hidden @internal
1141
     */
1142
    public enterEditMode(expressionItem: ExpressionOperandItem) {
UNCOV
1143
        if (this._editedExpression) {
×
1144
            this._editedExpression.inEditMode = false;
×
1145
        }
1146

UNCOV
1147
        if (this.parentExpression) {
×
UNCOV
1148
            this.inEditModeChange.emit(this.parentExpression);
×
1149
        }
1150

UNCOV
1151
        expressionItem.hovered = false;
×
UNCOV
1152
        this.fields = this.selectedEntity ? this.selectedEntity.fields : null;
×
UNCOV
1153
        this.selectedField =
×
1154
            expressionItem.expression.fieldName ?
×
UNCOV
1155
                this.fields?.find(field => field.field === expressionItem.expression.fieldName)
×
1156
                : null;
UNCOV
1157
        this.selectedCondition =
×
1158
            expressionItem.expression.condition ?
×
1159
                expressionItem.expression.condition.name :
1160
                null;
UNCOV
1161
        this.searchValue.value = expressionItem.expression.searchVal instanceof Set ?
×
1162
            Array.from(expressionItem.expression.searchVal) :
1163
            expressionItem.expression.searchVal;
1164

UNCOV
1165
        expressionItem.inEditMode = true;
×
UNCOV
1166
        this._editedExpression = expressionItem;
×
UNCOV
1167
        this.cdr.detectChanges();
×
1168

UNCOV
1169
        this.entitySelectOverlaySettings.target = this.entitySelect.getEditElement();
×
UNCOV
1170
        this.entitySelectOverlaySettings.excludeFromOutsideClick = [this.entitySelect.getEditElement() as HTMLElement];
×
UNCOV
1171
        this.entitySelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
×
1172

UNCOV
1173
        if (this.returnFieldSelect) {
×
UNCOV
1174
            this.returnFieldSelectOverlaySettings.target = this.returnFieldSelect.getEditElement();
×
UNCOV
1175
            this.returnFieldSelectOverlaySettings.excludeFromOutsideClick = [this.returnFieldSelect.getEditElement() as HTMLElement];
×
UNCOV
1176
            this.returnFieldSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
×
1177
        }
UNCOV
1178
        if (this.fieldSelect) {
×
UNCOV
1179
            this.fieldSelectOverlaySettings.target = this.fieldSelect.getEditElement();
×
UNCOV
1180
            this.fieldSelectOverlaySettings.excludeFromOutsideClick = [this.fieldSelect.getEditElement() as HTMLElement];
×
UNCOV
1181
            this.fieldSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
×
1182
        }
UNCOV
1183
        if (this.conditionSelect) {
×
UNCOV
1184
            this.conditionSelectOverlaySettings.target = this.conditionSelect.getEditElement();
×
UNCOV
1185
            this.conditionSelectOverlaySettings.excludeFromOutsideClick = [this.conditionSelect.getEditElement() as HTMLElement];
×
UNCOV
1186
            this.conditionSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
×
1187
        }
1188

UNCOV
1189
        if (!this.selectedField) {
×
UNCOV
1190
            this.fieldSelect.input.nativeElement.focus();
×
UNCOV
1191
        } else if (this.selectedField.filters.condition(this.selectedCondition)?.isUnary) {
×
UNCOV
1192
            this.conditionSelect?.input.nativeElement.focus();
×
1193
        } else {
UNCOV
1194
            const input = this.searchValueInput?.nativeElement || this.picker?.getEditElement();
×
UNCOV
1195
            input?.focus();
×
1196
        }
1197

UNCOV
1198
        (this.editingInputs?.nativeElement.parentElement as HTMLElement)?.scrollIntoView({ block: "nearest", inline: "nearest" });
×
1199
    }
1200

1201
    /**
1202
     * @hidden @internal
1203
     */
1204
    public onConditionSelectChanging(event: ISelectionEventArgs) {
UNCOV
1205
        event.cancel = true;
×
UNCOV
1206
        this.selectedCondition = event.newSelection.value;
×
UNCOV
1207
        this.conditionSelect.close();
×
UNCOV
1208
        this.cdr.detectChanges();
×
1209
    }
1210

1211
    /**
1212
     * @hidden @internal
1213
     */
1214
    public onKeyDown(eventArgs: KeyboardEvent) {
1215
        eventArgs.stopPropagation();
×
1216
    }
1217

1218
    /**
1219
     * @hidden @internal
1220
     */
1221
    public onGroupClick(groupContextMenuDropDown: any, targetButton: HTMLButtonElement, groupItem: ExpressionGroupItem) {
UNCOV
1222
        this.exitEditAddMode();
×
UNCOV
1223
        this.cdr.detectChanges();
×
1224

UNCOV
1225
        this.groupContextMenuDropDown = groupContextMenuDropDown;
×
UNCOV
1226
        this.groupContextMenuDropDownOverlaySettings.target = targetButton;
×
UNCOV
1227
        this.groupContextMenuDropDownOverlaySettings.positionStrategy = new ConnectedPositioningStrategy({
×
1228
            horizontalDirection: HorizontalAlignment.Right,
1229
            horizontalStartPoint: HorizontalAlignment.Left,
1230
            verticalStartPoint: VerticalAlignment.Bottom
1231
        });
1232

UNCOV
1233
        if (groupContextMenuDropDown.collapsed) {
×
UNCOV
1234
            this.contextualGroup = groupItem;
×
UNCOV
1235
            groupContextMenuDropDown.open(this.groupContextMenuDropDownOverlaySettings);
×
1236
        } else {
UNCOV
1237
            groupContextMenuDropDown.close();
×
1238
        }
1239
    }
1240

1241
    /**
1242
     * @hidden @internal
1243
     */
1244
    public getOperator(expressionItem: any) {
1245
        // if (!expressionItem && !this.expressionTree && !this.initialOperator) {
1246
        //     this.initialOperator = 0;
1247
        // }
1248

UNCOV
1249
        const operator = expressionItem ?
×
1250
            expressionItem.operator :
1251
            this.expressionTree ?
×
1252
                this.expressionTree.operator :
1253
                this.initialOperator;
UNCOV
1254
        return operator;
×
1255
    }
1256

1257
    /**
1258
     * @hidden @internal
1259
     */
1260
    public getSwitchGroupText(expressionItem: any) {
UNCOV
1261
        const operator = this.getOperator(expressionItem);
×
UNCOV
1262
        const condition = operator === FilteringLogic.Or ? this.resourceStrings.igx_query_builder_and_label : this.resourceStrings.igx_query_builder_or_label
×
UNCOV
1263
        return this.resourceStrings.igx_query_builder_switch_group.replace('{0}', condition.toUpperCase());
×
1264
    }
1265

1266
    /**
1267
     * @hidden @internal
1268
     */
1269
    public onGroupContextMenuDropDownSelectionChanging(event: ISelectionEventArgs) {
UNCOV
1270
        event.cancel = true;
×
1271

UNCOV
1272
        if (event.newSelection.value === 'switchCondition') {
×
UNCOV
1273
            const newOperator = (!this.expressionTree ? this.initialOperator : (this.contextualGroup ?? this._expressionTree).operator) === 0 ? 1 : 0;
×
UNCOV
1274
            this.selectFilteringLogic(newOperator);
×
UNCOV
1275
        } else if (event.newSelection.value === 'ungroup') {
×
UNCOV
1276
            this.ungroup();
×
1277
        }
1278

UNCOV
1279
        this.groupContextMenuDropDown.close();
×
1280
    }
1281

1282
    /**
1283
     * @hidden @internal
1284
     */
1285
    public ungroup() {
UNCOV
1286
        const selectedGroup = this.contextualGroup;
×
UNCOV
1287
        const parent = selectedGroup.parent;
×
UNCOV
1288
        if (parent) {
×
UNCOV
1289
            const index = parent.children.indexOf(selectedGroup);
×
UNCOV
1290
            parent.children.splice(index, 1, ...selectedGroup.children);
×
1291

UNCOV
1292
            for (const expr of selectedGroup.children) {
×
UNCOV
1293
                expr.parent = parent;
×
1294
            }
1295
        }
UNCOV
1296
        this.commitOperandEdit();
×
1297
    }
1298

1299
    /**
1300
     * @hidden @internal
1301
     */
1302
    public selectFilteringLogic(index: number) {
UNCOV
1303
        if (!this.expressionTree) {
×
1304
            this.initialOperator = index;
×
1305
            return;
×
1306
        }
1307

UNCOV
1308
        if (this.contextualGroup) {
×
UNCOV
1309
            this.contextualGroup.operator = index as FilteringLogic;
×
UNCOV
1310
            this.commitOperandEdit();
×
1311
        } else if (this.expressionTree) {
×
1312
            this._expressionTree.operator = index as FilteringLogic;
×
1313
        }
1314

UNCOV
1315
        this.initialOperator = null;
×
1316
    }
1317

1318
    /**
1319
     * @hidden @internal
1320
     */
1321
    public getConditionFriendlyName(name: string): string {
1322
        // As we have an 'In' condition already used in ESF to search in a Set, we add the 'Query' suffix to the newly introduced nested query condition names.
1323
        // So instead of in/notIn we end up with 'inQuery'/'notInQuery', hence removing the suffix from the friendly name.
UNCOV
1324
        return this.resourceStrings[`igx_query_builder_filter_${name?.replace('Query', '')}`] || name;
×
1325
    }
1326

1327
    /**
1328
     * @hidden @internal
1329
     */
1330
    public isDate(value: any) {
UNCOV
1331
        return value instanceof Date;
×
1332
    }
1333

1334
    /**
1335
     * @hidden @internal
1336
     */
1337
    public invokeClick(eventArgs: KeyboardEvent) {
UNCOV
1338
        if (!this.dragService.dropGhostExpression && this.platform.isActivationKey(eventArgs)) {
×
UNCOV
1339
            eventArgs.preventDefault();
×
UNCOV
1340
            (eventArgs.currentTarget as HTMLElement).click();
×
1341
        }
1342
    }
1343

1344
    /**
1345
     * @hidden @internal
1346
     */
1347
    public openPicker(args: KeyboardEvent) {
1348
        if (this.platform.isActivationKey(args)) {
×
1349
            args.preventDefault();
×
1350
            this.picker.open();
×
1351
        }
1352
    }
1353

1354
    /**
1355
     * @hidden @internal
1356
     */
1357
    public getConditionList(): string[] {
UNCOV
1358
        if (!this.selectedField) return [];
×
1359

UNCOV
1360
        if (!this.selectedField.filters) {
×
1361
            this.selectedField.filters = this.getFilters(this.selectedField);
×
1362
        }
1363

UNCOV
1364
        if ((this.isAdvancedFiltering() && !this.entities[0].childEntities) ||
×
1365
            (this.isHierarchicalNestedQuery() && this.selectedEntity.name && !this.selectedEntity.childEntities)) {
UNCOV
1366
            return this.selectedField.filters.conditionList();
×
1367
        }
1368

UNCOV
1369
        return this.selectedField.filters.extendedConditionList();
×
1370
    }
1371

1372
    /**
1373
     * @hidden @internal
1374
     */
1375
    public getFormatter(field: string) {
UNCOV
1376
        return this.fields?.find(el => el.field === field)?.formatter;
×
1377
    }
1378

1379
    /**
1380
     * @hidden @internal
1381
     */
1382
    public getFormat(field: string) {
UNCOV
1383
        return this.fields?.find(el => el.field === field).pipeArgs.format;
×
1384
    }
1385

1386
    /**
1387
     * @hidden @internal
1388
     *
1389
     * used by the grid
1390
     */
1391
    public setAddButtonFocus() {
UNCOV
1392
        if (this.addRootAndGroupButton) {
×
1393
            this.addRootAndGroupButton.nativeElement.focus();
×
UNCOV
1394
        } else if (this.addConditionButton) {
×
UNCOV
1395
            this.addConditionButton.nativeElement.focus();
×
1396
        }
1397
    }
1398

1399
    /**
1400
     * @hidden @internal
1401
     */
1402
    public context(expression: ExpressionItem, afterExpression?: ExpressionItem) {
UNCOV
1403
        return {
×
1404
            $implicit: expression,
1405
            afterExpression
1406
        };
1407
    }
1408

1409
    public formatReturnFields(innerTree: IFilteringExpressionsTree) {
UNCOV
1410
        const returnFields = innerTree.returnFields;
×
UNCOV
1411
        let text = returnFields.join(', ');
×
UNCOV
1412
        const innerTreeEntity = this.entities?.find(el => el.name === innerTree.entity);
×
UNCOV
1413
        if (returnFields.length === innerTreeEntity?.fields.length) {
×
1414
            text = this.resourceStrings.igx_query_builder_all_fields;
×
1415
        } else {
UNCOV
1416
            text = returnFields.join(', ');
×
UNCOV
1417
            text = text.length > 25 ? text.substring(0, 25) + ' ...' : text;
×
1418
        }
UNCOV
1419
        return text;
×
1420
    }
1421

1422
    public isInEditMode(): boolean {
UNCOV
1423
        return !this.parentExpression || (this.parentExpression && this.parentExpression.inEditMode);
×
1424
    }
1425

1426
    public onInEditModeChanged(expressionItem: ExpressionOperandItem) {
UNCOV
1427
        if (!expressionItem.inEditMode) {
×
1428
            this.enterExpressionEdit(expressionItem);
×
1429
        }
1430
    }
1431

1432
    public getExpressionTreeCopy(expressionTree: IExpressionTree, shouldAssignInnerQueryExprTree?: boolean): IExpressionTree {
UNCOV
1433
        if (!expressionTree) {
×
UNCOV
1434
            return null;
×
1435
        }
1436

UNCOV
1437
        const exprTreeCopy = new FilteringExpressionsTree(expressionTree.operator, expressionTree.fieldName, expressionTree.entity, expressionTree.returnFields);
×
UNCOV
1438
        exprTreeCopy.filteringOperands = [];
×
1439

UNCOV
1440
        expressionTree.filteringOperands.forEach(o => isTree(o) ? exprTreeCopy.filteringOperands.push(this.getExpressionTreeCopy(o)) : exprTreeCopy.filteringOperands.push(o));
×
1441

UNCOV
1442
        if (!this.innerQueryNewExpressionTree && shouldAssignInnerQueryExprTree) {
×
UNCOV
1443
            this.innerQueryNewExpressionTree = exprTreeCopy;
×
1444
        }
1445

UNCOV
1446
        return exprTreeCopy;
×
1447
    }
1448

1449
    public onSelectAllClicked() {
UNCOV
1450
        if (
×
1451
            (this._selectedReturnFields.length > 0 && this._selectedReturnFields.length < this._selectedEntity.fields.length) ||
×
1452
            this._selectedReturnFields.length == this._selectedEntity.fields.length
1453
        ) {
UNCOV
1454
            this.returnFieldsCombo.deselectAllItems();
×
1455
        } else {
UNCOV
1456
            this.returnFieldsCombo.selectAllItems();
×
1457
        }
1458
    }
1459

1460
    public onReturnFieldSelectChanging(event: IComboSelectionChangingEventArgs | ISelectionEventArgs) {
UNCOV
1461
        let newSelection = [];
×
UNCOV
1462
        if (Array.isArray(event.newSelection)) {
×
UNCOV
1463
            newSelection = event.newSelection.map(item => item.field)
×
1464
        } else {
UNCOV
1465
            newSelection.push(event.newSelection.value);
×
UNCOV
1466
            this._selectedReturnFields = newSelection;
×
1467
        }
1468

UNCOV
1469
        this.initExpressionTree(this.selectedEntity.name, newSelection);
×
1470
    }
1471

1472
    public initExpressionTree(selectedEntityName: string, selectedReturnFields: string[]) {
UNCOV
1473
        if (!this._expressionTree) {
×
UNCOV
1474
            this._expressionTree = this.createExpressionTreeFromGroupItem(new ExpressionGroupItem(FilteringLogic.And, this.rootGroup), selectedEntityName, selectedReturnFields);
×
1475
        }
1476

UNCOV
1477
        if (!this.parentExpression) {
×
UNCOV
1478
            this.expressionTreeChange.emit(this._expressionTree);
×
1479
        }
1480
    }
1481

1482
    public getSearchValueTemplateContext(defaultSearchValueTemplate): IgxQueryBuilderSearchValueContext {
UNCOV
1483
        const ctx = {
×
1484
            $implicit: this.searchValue,
1485
            selectedField: this.selectedField,
1486
            selectedCondition: this.selectedCondition,
1487
            defaultSearchValueTemplate: defaultSearchValueTemplate
1488
        };
UNCOV
1489
        return ctx;
×
1490
    }
1491

1492
    private getPipeArgs(field: FieldType) {
UNCOV
1493
        let pipeArgs = {...field.pipeArgs};
×
UNCOV
1494
        if (!pipeArgs) {
×
1495
            pipeArgs = { digitsInfo: DEFAULT_PIPE_DIGITS_INFO };
×
1496
        }
1497

UNCOV
1498
        if (!pipeArgs.format) {
×
UNCOV
1499
            pipeArgs.format = field.dataType === GridColumnDataType.Time ?
×
1500
                DEFAULT_PIPE_TIME_FORMAT : field.dataType === GridColumnDataType.DateTime ?
×
1501
                    DEFAULT_PIPE_DATE_TIME_FORMAT : DEFAULT_PIPE_DATE_FORMAT;
1502
        }
1503

UNCOV
1504
        return pipeArgs;
×
1505
    }
1506

1507
    private selectDefaultCondition() {
UNCOV
1508
        if (this.selectedField && this.selectedField.filters) {
×
UNCOV
1509
            this.selectedCondition = this.selectedField.filters.conditionList().indexOf('equals') >= 0 ? 'equals' : this.selectedField.filters.conditionList()[0];
×
1510
        }
1511
    }
1512

1513
    private getFilters(field: FieldType) {
UNCOV
1514
        if (!field.filters) {
×
UNCOV
1515
            switch (field.dataType) {
×
1516
                case GridColumnDataType.Boolean:
UNCOV
1517
                    return IgxBooleanFilteringOperand.instance();
×
1518
                case GridColumnDataType.Number:
1519
                case GridColumnDataType.Currency:
1520
                case GridColumnDataType.Percent:
UNCOV
1521
                    return IgxNumberFilteringOperand.instance();
×
1522
                case GridColumnDataType.Date:
UNCOV
1523
                    return IgxDateFilteringOperand.instance();
×
1524
                case GridColumnDataType.Time:
1525
                    return IgxTimeFilteringOperand.instance();
×
1526
                case GridColumnDataType.DateTime:
1527
                    return IgxDateTimeFilteringOperand.instance();
×
1528
                case GridColumnDataType.String:
1529
                default:
UNCOV
1530
                    return IgxStringFilteringOperand.instance();
×
1531
            }
1532
        } else {
UNCOV
1533
            return field.filters;
×
1534
        }
1535
    }
1536

1537

1538
    private addGroup(operator: FilteringLogic, parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
UNCOV
1539
        this.cancelOperandAdd();
×
1540

UNCOV
1541
        const groupItem = new ExpressionGroupItem(operator, parent);
×
1542

UNCOV
1543
        if (parent) {
×
UNCOV
1544
            if (afterExpression) {
×
1545
                const index = parent.children.indexOf(afterExpression);
×
1546
                parent.children.splice(index + 1, 0, groupItem);
×
1547
            } else {
UNCOV
1548
                parent.children.push(groupItem);
×
1549
            }
1550
        } else {
1551
            this.rootGroup = groupItem;
×
1552
        }
1553

UNCOV
1554
        this.addCondition(groupItem);
×
UNCOV
1555
        this.currentGroup = groupItem;
×
1556
    }
1557

1558
    private createExpressionGroupItem(expressionTree: IExpressionTree, parent?: ExpressionGroupItem, entityName?: string): ExpressionGroupItem {
1559
        let groupItem: ExpressionGroupItem;
UNCOV
1560
        if (expressionTree) {
×
UNCOV
1561
            groupItem = new ExpressionGroupItem(expressionTree.operator, parent);
×
UNCOV
1562
            if (!expressionTree.filteringOperands) {
×
1563
                return groupItem;
×
1564
            }
1565

UNCOV
1566
            for (let i = 0; i < expressionTree.filteringOperands.length; i++) {
×
UNCOV
1567
                const expr = expressionTree.filteringOperands[i];
×
1568

UNCOV
1569
                if (isTree(expr)) {
×
UNCOV
1570
                    groupItem.children.push(this.createExpressionGroupItem(expr, groupItem, expressionTree.entity));
×
1571
                } else {
UNCOV
1572
                    const filteringExpr = expr as IFilteringExpression;
×
UNCOV
1573
                    const exprCopy: IFilteringExpression = {
×
1574
                        fieldName: filteringExpr.fieldName,
1575
                        condition: filteringExpr.condition,
1576
                        conditionName: filteringExpr.condition?.name || filteringExpr.conditionName,
×
1577
                        searchVal: filteringExpr.searchVal,
1578
                        searchTree: filteringExpr.searchTree,
1579
                        ignoreCase: filteringExpr.ignoreCase
1580
                    };
UNCOV
1581
                    const operandItem = new ExpressionOperandItem(exprCopy, groupItem);
×
UNCOV
1582
                    const field = this.fields?.find(el => el.field === filteringExpr.fieldName);
×
UNCOV
1583
                    operandItem.fieldLabel = field?.label || field?.header || field?.field;
×
UNCOV
1584
                    if (this._expandedExpressions.filter(e => e.searchTree == operandItem.expression.searchTree).length > 0) {
×
1585
                        operandItem.expanded = true;
×
1586
                    }
UNCOV
1587
                    groupItem.children.push(operandItem);
×
1588
                }
1589
            }
1590

1591

UNCOV
1592
            if (expressionTree.entity) {
×
UNCOV
1593
                entityName = expressionTree.entity;
×
1594
            }
UNCOV
1595
            const entity = this.entities?.find(el => el.name === entityName);
×
UNCOV
1596
            if (entity) {
×
UNCOV
1597
                this.fields = entity.fields;
×
1598
            }
1599

UNCOV
1600
            this._selectedEntity = this.entities?.find(el => el.name === entityName);
×
UNCOV
1601
            this._selectedReturnFields =
×
1602
                !expressionTree.returnFields || expressionTree.returnFields.includes('*') || expressionTree.returnFields.includes('All') || expressionTree.returnFields.length === 0
×
UNCOV
1603
                    ? this.fields?.map(f => f.field)
×
UNCOV
1604
                    : this.fields?.filter(f => expressionTree.returnFields.indexOf(f.field) >= 0).map(f => f.field);
×
1605
        }
UNCOV
1606
        return groupItem;
×
1607
    }
1608

1609
    private createExpressionTreeFromGroupItem(groupItem: ExpressionGroupItem, entity?: string, returnFields?: string[]): FilteringExpressionsTree {
UNCOV
1610
        if (!groupItem) {
×
UNCOV
1611
            return null;
×
1612
        }
1613

UNCOV
1614
        const expressionTree = new FilteringExpressionsTree(groupItem.operator, undefined, entity, returnFields);
×
1615

UNCOV
1616
        for (let i = 0; i < groupItem.children.length; i++) {
×
UNCOV
1617
            const item = groupItem.children[i];
×
1618

UNCOV
1619
            if (item instanceof ExpressionGroupItem) {
×
UNCOV
1620
                const subTree = this.createExpressionTreeFromGroupItem((item as ExpressionGroupItem), entity, returnFields);
×
UNCOV
1621
                expressionTree.filteringOperands.push(subTree);
×
1622
            } else {
UNCOV
1623
                expressionTree.filteringOperands.push((item as ExpressionOperandItem).expression);
×
1624
            }
1625
        }
1626

UNCOV
1627
        return expressionTree;
×
1628
    }
1629

1630
    private scrollElementIntoView(target: HTMLElement) {
UNCOV
1631
        const container = this.expressionsContainer.nativeElement;
×
UNCOV
1632
        const targetOffset = target.offsetTop - container.offsetTop;
×
UNCOV
1633
        const delta = 10;
×
1634

UNCOV
1635
        if (container.scrollTop + delta > targetOffset) {
×
UNCOV
1636
            container.scrollTop = targetOffset - delta;
×
UNCOV
1637
        } else if (container.scrollTop + container.clientHeight < targetOffset + target.offsetHeight + delta) {
×
UNCOV
1638
            container.scrollTop = targetOffset + target.offsetHeight + delta - container.clientHeight;
×
1639
        }
1640
    }
1641

1642
    private focusEditedExpressionChip() {
UNCOV
1643
        if (this._timeoutId) {
×
UNCOV
1644
            clearTimeout(this._timeoutId);
×
1645
        }
1646

UNCOV
1647
        this._timeoutId = setTimeout(() => {
×
UNCOV
1648
            if (this._lastFocusedChipIndex != -1) {
×
1649
                //Sort the expression chip list.
1650
                //If there was a recent drag&drop and the tree hasn't rerendered(child query), they will be unordered
UNCOV
1651
                const sortedChips = this.expressionsChips.toArray().sort(function (a, b) {
×
UNCOV
1652
                    if (a === b) return 0;
×
UNCOV
1653
                    if (a.chipArea.nativeElement.compareDocumentPosition(b.chipArea.nativeElement) & 2) {
×
1654
                        // b comes before a
UNCOV
1655
                        return 1;
×
1656
                    }
UNCOV
1657
                    return -1;
×
1658
                });
UNCOV
1659
                const chipElement = sortedChips[this._lastFocusedChipIndex]?.nativeElement;
×
UNCOV
1660
                if (chipElement) {
×
UNCOV
1661
                    chipElement.focus();
×
1662
                }
UNCOV
1663
                this._lastFocusedChipIndex = -1;
×
UNCOV
1664
                this._focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
×
1665
            }
1666
        }, this._focusDelay);
1667
    }
1668

1669
    private init() {
UNCOV
1670
        this.cancelOperandAdd();
×
UNCOV
1671
        this.cancelOperandEdit();
×
1672

1673
        // Ignore values of certain properties for the comparison
UNCOV
1674
        const propsToIgnore = ['parent', 'hovered', 'ignoreCase', 'inEditMode', 'inAddMode'];
×
UNCOV
1675
        const propsReplacer = function replacer(key, value) {
×
UNCOV
1676
            if (propsToIgnore.indexOf(key) >= 0) {
×
UNCOV
1677
                return undefined;
×
1678
            } else {
UNCOV
1679
                return value;
×
1680
            }
1681
        };
1682

1683
        // Skip root being recreated if the same
UNCOV
1684
        const newRootGroup = this.createExpressionGroupItem(this.expressionTree);
×
UNCOV
1685
        if (JSON.stringify(this.rootGroup, propsReplacer) !== JSON.stringify(newRootGroup, propsReplacer)) {
×
UNCOV
1686
            this.rootGroup = this.createExpressionGroupItem(this.expressionTree);
×
UNCOV
1687
            this.currentGroup = this.rootGroup;
×
1688
        }
1689

UNCOV
1690
        if (this.rootGroup?.children?.length == 0) {
×
UNCOV
1691
            this.rootGroup = null;
×
UNCOV
1692
            this.currentGroup = null;
×
1693
        }
1694
    }
1695

1696
    private initLocale() {
UNCOV
1697
        this._defaultLocale = getCurrentI18n();
×
UNCOV
1698
        this._locale = this._localeId !== DEFAULT_LOCALE ? this._localeId : this._locale;
×
UNCOV
1699
        onResourceChangeHandle(this.destroy$, this.onResourceChange, this);
×
1700
    }
1701

1702
    private onResourceChange(args: CustomEvent<IResourceChangeEventArgs>) {
UNCOV
1703
        this._defaultLocale = args.detail.newLocale;
×
UNCOV
1704
        if (!this._locale) {
×
UNCOV
1705
            this._defaultResourceStrings = getCurrentResourceStrings(QueryBuilderResourceStringsEN, false);
×
1706
        }
1707
    }
1708

1709
    /** rootGroup is recreated after clicking Apply, which sets new expressionTree and calls init()*/
UNCOV
1710
    protected trackExpressionItem = trackByIdentity;
×
1711
}
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

© 2026 Coveralls, Inc