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

IgniteUI / igniteui-angular / 14086769991

26 Mar 2025 03:09PM UTC coverage: 91.594%. First build
14086769991

Pull #15607

github

web-flow
Merge 00cabcd80 into 141ca8c3b
Pull Request #15607: fix(query-builder): prevent fields collection change

13355 of 15618 branches covered (85.51%)

11 of 14 new or added lines in 1 file covered. (78.57%)

26913 of 29383 relevant lines covered (91.59%)

33922.27 hits per line

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

92.35
/projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.ts
1
import {
2
    AfterViewInit,
3
    EventEmitter,
4
    LOCALE_ID,
5
    Output,
6
    TemplateRef
7
} from '@angular/core';
8
import { getLocaleFirstDayOfWeek, NgTemplateOutlet, NgClass, DatePipe } from '@angular/common';
9
import { Inject } from '@angular/core';
10
import {
11
    Component, Input, ViewChild, ChangeDetectorRef, ViewChildren, QueryList, ElementRef, OnDestroy, HostBinding
12
} from '@angular/core';
13
import { FormsModule } from '@angular/forms';
14
import { Subject } from 'rxjs';
15
import { IgxChipComponent } from '../chips/chip.component';
16
import { IQueryBuilderResourceStrings, QueryBuilderResourceStringsEN } from '../core/i18n/query-builder-resources';
17
import { PlatformUtil, trackByIdentity } from '../core/utils';
18
import { DataType, DataUtil } from '../data-operations/data-util';
19
import { IgxBooleanFilteringOperand, IgxDateFilteringOperand, IgxDateTimeFilteringOperand, IgxNumberFilteringOperand, IgxStringFilteringOperand, IgxTimeFilteringOperand } from '../data-operations/filtering-condition';
20
import { FilteringLogic, IFilteringExpression } from '../data-operations/filtering-expression.interface';
21
import { FilteringExpressionsTree, IExpressionTree, IFilteringExpressionsTree } from '../data-operations/filtering-expressions-tree';
22
import { IgxDatePickerComponent } from '../date-picker/date-picker.component';
23

24
import { IgxButtonDirective } from '../directives/button/button.directive';
25
import { IgxDateTimeEditorDirective } from '../directives/date-time-editor/date-time-editor.directive';
26

27
import { IgxOverlayOutletDirective } from '../directives/toggle/toggle.directive';
28
import { FieldType, EntityType } from '../grids/common/grid.interface';
29
import { IgxSelectComponent } from '../select/select.component';
30
import { HorizontalAlignment, OverlaySettings, VerticalAlignment } from '../services/overlay/utilities';
31
import { AbsoluteScrollStrategy, AutoPositionStrategy, CloseScrollStrategy, ConnectedPositioningStrategy } from '../services/public_api';
32
import { IgxTimePickerComponent } from '../time-picker/time-picker.component';
33
import { IgxPickerToggleComponent, IgxPickerClearComponent } from '../date-common/picker-icons.common';
34
import { IgxInputDirective } from '../directives/input/input.directive';
35
import { IgxInputGroupComponent } from '../input-group/input-group.component';
36
import { IgxSelectItemComponent } from '../select/select-item.component';
37
import { IgxPrefixDirective } from '../directives/prefix/prefix.directive';
38
import { IgxIconComponent } from '../icon/icon.component';
39
import { getCurrentResourceStrings } from '../core/i18n/resources';
40
import { IgxIconButtonDirective } from '../directives/button/icon-button.directive';
41
import { IComboSelectionChangingEventArgs, IgxComboComponent } from "../combo/combo.component";
42
import { IgxComboHeaderDirective } from '../combo/public_api';
43
import { IgxCheckboxComponent } from "../checkbox/checkbox.component";
44
import { IChangeCheckboxEventArgs } from '../checkbox/checkbox-base.directive';
45
import { IgxDialogComponent } from "../dialog/dialog.component";
46
import { ISelectionEventArgs } from '../drop-down/drop-down.common';
47
import { IgxTooltipDirective } from '../directives/tooltip/tooltip.directive';
48
import { IgxTooltipTargetDirective } from '../directives/tooltip/tooltip-target.directive';
49
import { IgxQueryBuilderSearchValueTemplateDirective } from './query-builder.directives';
50
import { IgxQueryBuilderComponent } from './query-builder.component';
51
import { IgxDragIgnoreDirective, IgxDropDirective } from '../directives/drag-drop/drag-drop.directive';
52
import { IgxDropDownComponent } from '../drop-down/drop-down.component';
53
import { IgxDropDownItemComponent } from '../drop-down/drop-down-item.component';
54
import { IgxDropDownItemNavigationDirective } from '../drop-down/drop-down-navigation.directive';
55
import { IgxQueryBuilderDragService } from './query-builder-drag.service';
56
import { isTree } from '../data-operations/expressions-tree-util';
57
import { ExpressionGroupItem, ExpressionItem, ExpressionOperandItem, IgxFieldFormatterPipe } from './query-builder.common';
58

59
const DEFAULT_PIPE_DATE_FORMAT = 'mediumDate';
3✔
60
const DEFAULT_PIPE_TIME_FORMAT = 'mediumTime';
3✔
61
const DEFAULT_PIPE_DATE_TIME_FORMAT = 'medium';
3✔
62
const DEFAULT_PIPE_DIGITS_INFO = '1.0-3';
3✔
63
const DEFAULT_CHIP_FOCUS_DELAY = 50;
3✔
64

65
/** @hidden */
66
@Component({
67
    selector: 'igx-query-builder-tree',
68
    templateUrl: './query-builder-tree.component.html',
69
    host: { 'class': 'igx-query-builder-tree' },
70
    imports: [
71
        DatePipe,
72
        FormsModule,
73
        IgxButtonDirective,
74
        IgxCheckboxComponent,
75
        IgxChipComponent,
76
        IgxComboComponent,
77
        IgxComboHeaderDirective,
78
        IgxDatePickerComponent,
79
        IgxDateTimeEditorDirective,
80
        IgxDialogComponent,
81
        IgxDragIgnoreDirective,
82
        IgxDropDirective,
83
        IgxDropDownComponent,
84
        IgxDropDownItemComponent,
85
        IgxDropDownItemNavigationDirective,
86
        IgxFieldFormatterPipe,
87
        IgxIconButtonDirective,
88
        IgxIconComponent,
89
        IgxInputDirective,
90
        IgxInputGroupComponent,
91
        IgxOverlayOutletDirective,
92
        IgxPickerClearComponent,
93
        IgxPickerToggleComponent,
94
        IgxPrefixDirective,
95
        IgxSelectComponent,
96
        IgxSelectItemComponent,
97
        IgxTimePickerComponent,
98
        IgxTooltipDirective,
99
        IgxTooltipTargetDirective,
100
        NgClass,
101
        NgTemplateOutlet
102
    ],
103
    providers: [
104
        IgxQueryBuilderDragService
105
    ],
106
})
107
export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
3✔
108
    /**
109
     * @hidden @internal
110
     */
111
    public _expressionTree: IExpressionTree;
112

113
    /**
114
     * @hidden @internal
115
     */
116
    public _expressionTreeCopy: IExpressionTree;
117

118
    /**
119
     * @hidden @internal
120
     */
121
    @HostBinding('class') public get getClass() {
122
        return `igx-query-builder-tree--level-${this.level}`;
3,421✔
123
    }
124

125
    /**
126
     * Sets/gets the entities.
127
     */
128
    @Input()
129
    public entities: EntityType[];
130

131
    /**
132
     * Sets/gets the parent query builder component.
133
     */
134
    @Input()
135
    public queryBuilder: IgxQueryBuilderComponent;
136

137
    /**
138
     * Sets/gets the search value template.
139
     */
140
    @Input()
141
    public searchValueTemplate: TemplateRef<IgxQueryBuilderSearchValueTemplateDirective> = null;
235✔
142

143
    /**
144
    * Returns the parent expression operand.
145
    */
146
    @Input()
147
    public get parentExpression(): ExpressionOperandItem {
148
        return this._parentExpression;
41,761✔
149
    }
150

151
    /**
152
     * Sets the parent expression operand.
153
     */
154
    public set parentExpression(value: ExpressionOperandItem) {
155
        this._parentExpression = value;
94✔
156
    }
157

158
    /**
159
    * Returns the fields.
160
    */
161
    public get fields(): FieldType[] {
162
        if (!this._fields && this.isAdvancedFiltering()) {
18,782✔
163
            this._fields = this.entities[0].fields;
44✔
164
        }
165

166
        return this._fields;
18,782✔
167
    }
168

169
    /**
170
     * Sets the fields.
171
     */
172
    @Input()
173
    public set fields(fields: FieldType[]) {
174
        this._fields = fields;
686✔
175
        
176
        this._fields = this._fields?.map(f => ({...f, filters: this.getFilters(f), pipeArgs: this.getPipeArgs(f) }));
2,636✔
177
        
178
        if (!this._fields && this.isAdvancedFiltering()) {
686✔
179
            this._fields = this.entities[0].fields;
27✔
180
        }
181
    }
182

183
    /**
184
    * Returns the expression tree.
185
    */
186
    public get expressionTree(): IExpressionTree {
187
        return this._expressionTree;
5,763✔
188
    }
189

190
    /**
191
     * Sets the expression tree.
192
     */
193
    @Input()
194
    public set expressionTree(expressionTree: IExpressionTree) {
195
        this._expressionTree = expressionTree;
443✔
196
        if (!expressionTree) {
443✔
197
            this._selectedEntity = null;
143✔
198
            this._selectedReturnFields = [];
143✔
199
        }
200

201
        if (!this._preventInit) {
443✔
202
            this.init();
434✔
203
        }
204
    }
205

206
    /**
207
     * Gets the `locale` of the query builder.
208
     * If not set, defaults to application's locale.
209
     */
210
    @Input()
211
    public get locale(): string {
212
        return this._locale;
942✔
213
    }
214

215
    /**
216
     * Sets the `locale` of the query builder.
217
     * Expects a valid BCP 47 language tag.
218
     */
219
    public set locale(value: string) {
220
        this._locale = value;
376✔
221
        // if value is invalid, set it back to _localeId
222
        try {
376✔
223
            getLocaleFirstDayOfWeek(this._locale);
376✔
224
        } catch {
225
            this._locale = this._localeId;
97✔
226
        }
227
    }
228

229
    /**
230
     * Sets the resource strings.
231
     * By default it uses EN resources.
232
     */
233
    @Input()
234
    public set resourceStrings(value: IQueryBuilderResourceStrings) {
235
        this._resourceStrings = Object.assign({}, this._resourceStrings, value);
142✔
236
    }
237

238
    /**
239
     * Returns the resource strings.
240
     */
241
    public get resourceStrings(): IQueryBuilderResourceStrings {
242
        return this._resourceStrings;
93,291✔
243
    }
244

245
    /**
246
     * Event fired as the expression tree is changed.
247
     */
248
    @Output()
249
    public expressionTreeChange = new EventEmitter<IExpressionTree>();
235✔
250

251
    /**
252
     * Event fired if a nested query builder tree is being edited.
253
     */
254
    @Output()
255
    public inEditModeChange = new EventEmitter<ExpressionOperandItem>();
235✔
256

257
    @ViewChild('entitySelect', { read: IgxSelectComponent })
258
    protected entitySelect: IgxSelectComponent;
259

260
    @ViewChild('editingInputs', { read: ElementRef })
261
    private editingInputs: ElementRef;
262

263
    @ViewChild('returnFieldsCombo', { read: IgxComboComponent })
264
    private returnFieldsCombo: IgxComboComponent;
265

266
    @ViewChild('returnFieldSelect', { read: IgxSelectComponent })
267
    protected returnFieldSelect: IgxSelectComponent;
268

269
    @ViewChild('fieldSelect', { read: IgxSelectComponent })
270
    private fieldSelect: IgxSelectComponent;
271

272
    @ViewChild('conditionSelect', { read: IgxSelectComponent })
273
    private conditionSelect: IgxSelectComponent;
274

275
    @ViewChild('searchValueInput', { read: ElementRef })
276
    private searchValueInput: ElementRef;
277

278
    @ViewChild('picker')
279
    private picker: IgxDatePickerComponent | IgxTimePickerComponent;
280

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

284
    @ViewChild('addConditionButton', { read: ElementRef })
285
    private addConditionButton: ElementRef;
286

287
    @ViewChild('entityChangeDialog', { read: IgxDialogComponent })
288
    private entityChangeDialog: IgxDialogComponent;
289

290
    @ViewChild('addOptionsDropDown', { read: IgxDropDownComponent })
291
    private addExpressionItemDropDown: IgxDropDownComponent;
292

293
    @ViewChild('groupContextMenuDropDown', { read: IgxDropDownComponent })
294
    private groupContextMenuDropDown: IgxDropDownComponent;
295

296
    /**
297
     * @hidden @internal
298
     */
299
    @ViewChildren(IgxChipComponent, { read: IgxChipComponent })
300
    public expressionsChips: QueryList<IgxChipComponent>;
301

302
    @ViewChild('editingInputsContainer', { read: ElementRef })
303
    protected set editingInputsContainer(value: ElementRef) {
304
        if ((value && !this._editingInputsContainer) ||
671✔
305
            (value && this._editingInputsContainer && this._editingInputsContainer.nativeElement !== value.nativeElement)) {
306
            requestAnimationFrame(() => {
230✔
307
                this.scrollElementIntoView(value.nativeElement);
230✔
308
            });
309
        }
310

311
        this._editingInputsContainer = value;
671✔
312
    }
313

314
    /** @hidden */
315
    protected get editingInputsContainer(): ElementRef {
316
        return this._editingInputsContainer;
×
317
    }
318

319
    @ViewChild('currentGroupButtonsContainer', { read: ElementRef })
320
    protected set currentGroupButtonsContainer(value: ElementRef) {
321
        if ((value && !this._currentGroupButtonsContainer) ||
671✔
322
            (value && this._currentGroupButtonsContainer && this._currentGroupButtonsContainer.nativeElement !== value.nativeElement)) {
323
            requestAnimationFrame(() => {
306✔
324
                this.scrollElementIntoView(value.nativeElement);
306✔
325
            });
326
        }
327

328
        this._currentGroupButtonsContainer = value;
671✔
329
    }
330

331
    /** @hidden */
332
    protected get currentGroupButtonsContainer(): ElementRef {
333
        return this._currentGroupButtonsContainer;
×
334
    }
335

336
    @ViewChild('expressionsContainer')
337
    private expressionsContainer: ElementRef;
338

339
    @ViewChild('overlayOutlet', { read: IgxOverlayOutletDirective, static: true })
340
    private overlayOutlet: IgxOverlayOutletDirective;
341

342
    @ViewChildren(IgxQueryBuilderTreeComponent)
343
    private innerQueries: QueryList<IgxQueryBuilderTreeComponent>;
344

345
    /**
346
     * @hidden @internal
347
     */
348
    public innerQueryNewExpressionTree: IExpressionTree;
349

350
    /**
351
     * @hidden @internal
352
     */
353
    public rootGroup: ExpressionGroupItem;
354

355
    /**
356
     * @hidden @internal
357
     */
358
    public selectedExpressions: ExpressionOperandItem[] = [];
235✔
359

360
    /**
361
     * @hidden @internal
362
     */
363
    public currentGroup: ExpressionGroupItem;
364

365
    /**
366
     * @hidden @internal
367
     */
368
    public contextualGroup: ExpressionGroupItem;
369

370
    /**
371
     * @hidden @internal
372
     */
373
    public filteringLogics;
374

375
    /**
376
     * @hidden @internal
377
     */
378
    public selectedCondition: string;
379

380
    /**
381
     * @hidden @internal
382
     */
383
    public searchValue: { value: any } = { value: null };
235✔
384

385
    /**
386
     * @hidden @internal
387
     */
388
    public pickerOutlet: IgxOverlayOutletDirective | ElementRef;
389

390
    /**
391
     * @hidden @internal
392
     */
393
    public prevFocusedExpression: ExpressionOperandItem;
394

395
    /**
396
     * @hidden @internal
397
     */
398
    public initialOperator = 0;
235✔
399

400
    /**
401
     * @hidden @internal
402
     */
403
    public returnFieldSelectOverlaySettings: OverlaySettings = {
235✔
404
        scrollStrategy: new AbsoluteScrollStrategy(),
405
        modal: false,
406
        closeOnOutsideClick: true
407
    };
408

409
    /**
410
     * @hidden @internal
411
     */
412
    public entitySelectOverlaySettings: OverlaySettings = {
235✔
413
        scrollStrategy: new AbsoluteScrollStrategy(),
414
        modal: false,
415
        closeOnOutsideClick: true
416
    };
417

418
    /**
419
     * @hidden @internal
420
     */
421
    public fieldSelectOverlaySettings: OverlaySettings = {
235✔
422
        scrollStrategy: new AbsoluteScrollStrategy(),
423
        modal: false,
424
        closeOnOutsideClick: true
425
    };
426

427
    /**
428
     * @hidden @internal
429
     */
430
    public conditionSelectOverlaySettings: OverlaySettings = {
235✔
431
        scrollStrategy: new AbsoluteScrollStrategy(),
432
        modal: false,
433
        closeOnOutsideClick: true
434
    };
435

436
    /**
437
     * @hidden @internal
438
     */
439
    public addExpressionDropDownOverlaySettings: OverlaySettings = {
235✔
440
        scrollStrategy: new AbsoluteScrollStrategy(),
441
        modal: false,
442
        closeOnOutsideClick: true
443
    };
444

445
    /**
446
     * @hidden @internal
447
     */
448
    public groupContextMenuDropDownOverlaySettings: OverlaySettings = {
235✔
449
        scrollStrategy: new AbsoluteScrollStrategy(),
450
        modal: false,
451
        closeOnOutsideClick: true
452
    };
453

454
    private destroy$ = new Subject<any>();
235✔
455
    private _timeoutId: any;
456
    private _lastFocusedChipIndex: number;
457
    private _focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
235✔
458
    private _parentExpression: ExpressionOperandItem;
459
    private _selectedEntity: EntityType;
460
    private _selectedReturnFields: string | string[];
461
    private _selectedField: FieldType;
462
    private _editingInputsContainer: ElementRef;
463
    private _currentGroupButtonsContainer: ElementRef;
464
    private _addModeExpression: ExpressionOperandItem;
465
    private _editedExpression: ExpressionOperandItem;
466
    private _preventInit = false;
235✔
467
    private _prevFocusedContainer: ElementRef;
468
    private _expandedExpressions: IFilteringExpression[] = [];
235✔
469
    private _fields: FieldType[];
470
    private _locale;
471
    private _entityNewValue: EntityType;
472
    private _resourceStrings = getCurrentResourceStrings(QueryBuilderResourceStringsEN);
235✔
473

474
    /**
475
     * Returns if the select entity dropdown at the root level is disabled after the initial selection.
476
     */
477
    public get disableEntityChange(): boolean {
478

479
        return !this.parentExpression && this.selectedEntity ? this.queryBuilder.disableEntityChange : false;
4,109✔
480
    }
481

482
    /**
483
     * Returns if the fields combo at the root level is disabled.
484
     */
485
    public get disableReturnFieldsChange(): boolean {
486

487
        return !this.selectedEntity || this.queryBuilder.disableReturnFieldsChange;
2,789✔
488
    }
489

490
    /**
491
     * Returns the current level.
492
     */
493
    public get level(): number {
494
        let parent = this.elRef.nativeElement.parentElement;
3,421✔
495
        let _level = 0;
3,421✔
496
        while (parent) {
3,421✔
497
            if (parent.localName === 'igx-query-builder-tree') {
24,774✔
498
                _level++;
1,174✔
499
            }
500
            parent = parent.parentElement;
24,774✔
501
        }
502
        return _level;
3,421✔
503
    }
504

505
    private _positionSettings = {
235✔
506
        horizontalStartPoint: HorizontalAlignment.Right,
507
        verticalStartPoint: VerticalAlignment.Top
508
    };
509

510
    private _overlaySettings: OverlaySettings = {
235✔
511
        closeOnOutsideClick: false,
512
        modal: false,
513
        positionStrategy: new ConnectedPositioningStrategy(this._positionSettings),
514
        scrollStrategy: new CloseScrollStrategy()
515
    };
516

517
    /** @hidden */
518
    protected isAdvancedFiltering(): boolean {
519
        return this.entities?.length === 1 && !this.entities[0]?.name;
7,769✔
520
    }
521

522
    /** @hidden */
523
    protected isSearchValueInputDisabled(): boolean {
524
        return !this.selectedField ||
1,481✔
525
            !this.selectedCondition ||
526
            (this.selectedField &&
527
                (this.selectedField.filters.condition(this.selectedCondition).isUnary ||
528
                    this.selectedField.filters.condition(this.selectedCondition).isNestedQuery));
529
    }
530

531
    constructor(public cdr: ChangeDetectorRef,
235✔
532
        public dragService: IgxQueryBuilderDragService,
235✔
533
        protected platform: PlatformUtil,
235✔
534
        private elRef: ElementRef,
235✔
535
        @Inject(LOCALE_ID) protected _localeId: string) {
235✔
536
        this.locale = this.locale || this._localeId;
235✔
537
        this.dragService.register(this, elRef);
235✔
538
    }
539

540
    /**
541
     * @hidden @internal
542
     */
543
    public ngAfterViewInit(): void {
544
        this._overlaySettings.outlet = this.overlayOutlet;
235✔
545
        this.entitySelectOverlaySettings.outlet = this.overlayOutlet;
235✔
546
        this.fieldSelectOverlaySettings.outlet = this.overlayOutlet;
235✔
547
        this.conditionSelectOverlaySettings.outlet = this.overlayOutlet;
235✔
548
        this.returnFieldSelectOverlaySettings.outlet = this.overlayOutlet;
235✔
549
        this.addExpressionDropDownOverlaySettings.outlet = this.overlayOutlet;
235✔
550
        this.groupContextMenuDropDownOverlaySettings.outlet = this.overlayOutlet;
235✔
551
        // Trigger additional change detection cycle
552
        this.cdr.detectChanges();
235✔
553
    }
554

555
    /**
556
     * @hidden @internal
557
     */
558
    public ngOnDestroy(): void {
559
        this.destroy$.next(true);
235✔
560
        this.destroy$.complete();
235✔
561
    }
562

563
    /**
564
     * @hidden @internal
565
     */
566
    public set selectedEntity(value: string) {
567
        this._selectedEntity = this.entities?.find(el => el.name === value);
×
568
    }
569

570
    /**
571
     * @hidden @internal
572
     */
573
    public get selectedEntity(): EntityType {
574
        return this._selectedEntity;
29,080✔
575
    }
576

577
    /**
578
     * @hidden @internal
579
     */
580
    public onEntitySelectChanging(event: ISelectionEventArgs) {
581
        event.cancel = true;
42✔
582
        this._entityNewValue = event.newSelection.value;
42✔
583
        if (event.oldSelection.value && this.queryBuilder.showEntityChangeDialog) {
42✔
584
            this.entityChangeDialog.open();
6✔
585
        } else {
586
            this.onEntityChangeConfirm();
36✔
587
        }
588
    }
589

590
    /**
591
     * @hidden
592
     */
593
    public onShowEntityChangeDialogChange(eventArgs: IChangeCheckboxEventArgs) {
594
        this.queryBuilder.showEntityChangeDialog = !eventArgs.checked;
1✔
595
    }
596

597
    /**
598
     * @hidden
599
     */
600
    public onEntityChangeCancel() {
601
        this.entityChangeDialog.close();
3✔
602
        this.entitySelect.close();
3✔
603
        this._entityNewValue = null;
3✔
604
    }
605

606
    /**
607
     * @hidden
608
     */
609
    public onEntityChangeConfirm() {
610
        if (this._parentExpression) {
38✔
611
            this._expressionTree = this.createExpressionTreeFromGroupItem(this.createExpressionGroupItem(this._expressionTree));
5✔
612
        }
613

614
        this._selectedEntity = this._entityNewValue;
38✔
615
        if (!this._selectedEntity.fields) {
38!
616
            this._selectedEntity.fields = [];
×
617
        }
618
        this.fields = this._entityNewValue ? this._entityNewValue.fields : [];
38!
619

620
        this._selectedReturnFields = this.parentExpression ? [] : this._entityNewValue.fields?.map(f => f.field);
132✔
621

622
        if (this._expressionTree) {
38✔
623
            this._expressionTree.entity = this._entityNewValue.name;
5✔
624

625
            this._expressionTree.returnFields = this.fields.length === this._selectedReturnFields.length ? ['*'] : this._selectedReturnFields;
5✔
626

627
            this._expressionTree.filteringOperands = [];
5✔
628

629
            this._editedExpression = null;
5✔
630
            if (!this.parentExpression) {
5✔
631
                this.expressionTreeChange.emit(this._expressionTree);
4✔
632
            }
633

634
            this.rootGroup = null;
5✔
635
            this.currentGroup = this.rootGroup;
5✔
636
        }
637

638
        this._selectedField = null;
38✔
639
        this.selectedCondition = null;
38✔
640
        this.searchValue.value = null;
38✔
641

642
        this.entityChangeDialog.close();
38✔
643
        this.entitySelect.close();
38✔
644

645
        this._entityNewValue = null;
38✔
646
        this.innerQueryNewExpressionTree = null;
38✔
647

648
        this.initExpressionTree(this._selectedEntity.name, this.selectedReturnFields);
38✔
649
    }
650

651
    /**
652
     * @hidden @internal
653
     */
654
    public set selectedReturnFields(value: string[]) {
655
        if (this._selectedReturnFields !== value) {
5✔
656
            this._selectedReturnFields = value;
5✔
657

658
            if (this._expressionTree && !this.parentExpression) {
5✔
659
                this._expressionTree.returnFields = value.length === this.fields.length ? ['*'] : value;
5✔
660
                this.expressionTreeChange.emit(this._expressionTree);
5✔
661
            }
662
        }
663
    }
664

665
    /**
666
     * @hidden @internal
667
     */
668
    public get selectedReturnFields(): string[] {
669
        if (typeof this._selectedReturnFields == 'string') {
17,253!
670
            return [this._selectedReturnFields];
×
671
        }
672
        return this._selectedReturnFields;
17,253✔
673
    }
674

675
    /**
676
     * @hidden @internal
677
     */
678
    public set selectedField(value: FieldType) {
679
        const oldValue = this._selectedField;
185✔
680

681
        if (this._selectedField !== value) {
185✔
682
            this._selectedField = value;
153✔
683
            this.selectDefaultCondition();
153✔
684
            if (oldValue && this._selectedField && this._selectedField.dataType !== oldValue.dataType) {
153✔
685
                this.searchValue.value = null;
17✔
686
                this.cdr.detectChanges();
17✔
687
            }
688
        }
689
    }
690

691
    /**
692
     * @hidden @internal
693
     */
694
    public get selectedField(): FieldType {
695
        return this._selectedField;
43,488✔
696
    }
697

698
    /**
699
     * @hidden @internal
700
     *
701
     * used by the grid
702
     */
703
    public setPickerOutlet(outlet?: IgxOverlayOutletDirective | ElementRef) {
704
        this.pickerOutlet = outlet;
44✔
705
    }
706

707
    /**
708
     * @hidden @internal
709
     *
710
     * used by the grid
711
     */
712
    public get isContextMenuVisible(): boolean {
713
        return !this.groupContextMenuDropDown.collapsed;
×
714
    }
715

716
    /**
717
     * @hidden @internal
718
     */
719
    public get hasEditedExpression(): boolean {
720
        return this._editedExpression !== undefined && this._editedExpression !== null;
7,577✔
721
    }
722

723
    /**
724
     * @hidden @internal
725
     */
726
    public addCondition(parent: ExpressionGroupItem, afterExpression?: ExpressionOperandItem, isUIInteraction?: boolean) {
727
        this.cancelOperandAdd();
68✔
728

729
        const operandItem = new ExpressionOperandItem({
68✔
730
            fieldName: null,
731
            condition: null,
732
            conditionName: null,
733
            ignoreCase: true,
734
            searchVal: null
735
        }, parent);
736

737
        const groupItem = new ExpressionGroupItem(this.getOperator(null) ?? FilteringLogic.And, parent);
68!
738
        this.contextualGroup = groupItem;
68✔
739
        this.initialOperator = null;
68✔
740

741
        this._lastFocusedChipIndex = this._lastFocusedChipIndex === undefined ? -1 : this._lastFocusedChipIndex;
68✔
742

743
        if (parent) {
68✔
744
            if (afterExpression) {
11✔
745
                const index = parent.children.indexOf(afterExpression);
1✔
746
                parent.children.splice(index + 1, 0, operandItem);
1✔
747
            } else {
748
                parent.children.push(operandItem);
10✔
749
            }
750
            this._lastFocusedChipIndex++;
11✔
751
        } else {
752
            this.rootGroup = groupItem;
57✔
753
            operandItem.parent = groupItem;
57✔
754
            this.rootGroup.children.push(operandItem);
57✔
755
            this._lastFocusedChipIndex = 0;
57✔
756
        }
757

758
        this._focusDelay = 250;
68✔
759

760
        if (isUIInteraction && !afterExpression) {
68✔
761
            this._lastFocusedChipIndex = this.expressionsChips.length;
64✔
762
            this._focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
64✔
763
        }
764

765
        this.enterExpressionEdit(operandItem);
68✔
766
    }
767

768
    /**
769
     * @hidden @internal
770
     */
771
    public addReverseGroup(parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
772
        parent = parent ?? this.rootGroup;
3!
773

774
        if (parent.operator === FilteringLogic.And) {
3✔
775
            this.addGroup(FilteringLogic.Or, parent, afterExpression);
2✔
776
        } else {
777
            this.addGroup(FilteringLogic.And, parent, afterExpression);
1✔
778
        }
779
    }
780

781
    /**
782
     * @hidden @internal
783
     */
784
    public endGroup(groupItem: ExpressionGroupItem) {
785
        this.currentGroup = groupItem.parent;
×
786
    }
787

788
    /**
789
     * @hidden @internal
790
     */
791
    public commitExpression() {
792
        this.commitOperandEdit();
44✔
793
        this.focusEditedExpressionChip();
44✔
794
    }
795

796
    /**
797
     * @hidden @internal
798
     */
799
    public discardExpression(expressionItem?: ExpressionOperandItem) {
800
        this.cancelOperandEdit();
11✔
801
        if (expressionItem && expressionItem.expression.fieldName) {
11✔
802
            this.focusEditedExpressionChip();
4✔
803
        }
804
    }
805

806
    /**
807
     * @hidden @internal
808
     */
809
    public commitOperandEdit() {
810
        const actualSearchValue = this.searchValue.value;
59✔
811
        if (this._editedExpression) {
59✔
812
            this._editedExpression.expression.fieldName = this.selectedField.field;
56✔
813
            this._editedExpression.expression.condition = this.selectedField.filters.condition(this.selectedCondition);
56✔
814
            this._editedExpression.expression.conditionName = this.selectedCondition;
56✔
815
            this._editedExpression.expression.searchVal = DataUtil.parseValue(this.selectedField.dataType, actualSearchValue) || actualSearchValue;
56✔
816
            this._editedExpression.fieldLabel = this.selectedField.label
56!
817
                ? this.selectedField.label
818
                : this.selectedField.header
56✔
819
                    ? this.selectedField.header
820
                    : this.selectedField.field;
821

822
            const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0]
56✔
823
            if (innerQuery && this.selectedField?.filters?.condition(this.selectedCondition)?.isNestedQuery) {
56✔
824
                innerQuery.exitEditAddMode();
7✔
825
                this._editedExpression.expression.searchTree = this.getExpressionTreeCopy(innerQuery.expressionTree);
7✔
826
                this._editedExpression.expression.searchTree.returnFields = innerQuery.selectedReturnFields;
7✔
827
            } else {
828
                this._editedExpression.expression.searchTree = null;
49✔
829
            }
830
            this.innerQueryNewExpressionTree = null;
56✔
831

832
            if (this.selectedField.filters.condition(this.selectedCondition)?.isUnary || this.selectedField.filters.condition(this.selectedCondition)?.isNestedQuery) {
56✔
833
                this._editedExpression.expression.searchVal = null;
12✔
834
            }
835

836
            this._editedExpression.inEditMode = false;
56✔
837
            this._editedExpression = null;
56✔
838
        }
839

840
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup, this.selectedEntity?.name, this.selectedReturnFields);
59✔
841
        if (!this.parentExpression) {
59✔
842
            this.expressionTreeChange.emit(this._expressionTree);
50✔
843
        }
844
    }
845

846
    /**
847
     * @hidden @internal
848
     */
849
    public cancelOperandAdd() {
850
        if (this._addModeExpression) {
520!
851
            this._addModeExpression.inAddMode = false;
×
852
            this._addModeExpression = null;
×
853
        }
854
    }
855

856
    /**
857
     * @hidden @internal
858
     */
859
    public deleteItem = (expressionItem: ExpressionItem, skipEmit: boolean = false) => {
235✔
860
        if (!expressionItem.parent) {
48✔
861
            this.rootGroup = null;
15✔
862
            this.currentGroup = null;
15✔
863
            //this._expressionTree = null;
864
            return;
15✔
865
        }
866

867
        if (expressionItem === this.currentGroup) {
33!
868
            this.currentGroup = this.currentGroup.parent;
×
869
        }
870

871
        const children = expressionItem.parent.children;
33✔
872
        const index = children.indexOf(expressionItem);
33✔
873
        children.splice(index, 1);
33✔
874
        const entity = this.expressionTree ? this.expressionTree.entity : null;
33✔
875
        const returnFields = this.expressionTree ? this.expressionTree.returnFields : null;
33✔
876
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup, entity, returnFields); // TODO: don't recreate if not necessary
33✔
877

878
        if (!children.length) {
33✔
879
            this.deleteItem(expressionItem.parent, true);
16✔
880
        }
881

882
        if (!this.parentExpression && !skipEmit) {
33✔
883
            this.expressionTreeChange.emit(this._expressionTree);
31✔
884
        }
885
    }
886

887
    /**
888
     * @hidden @internal
889
     */
890
    public cancelOperandEdit() {
891
        if (this.innerQueries) {
461✔
892
            const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
226✔
893
            if (innerQuery) {
226✔
894
                if (innerQuery._editedExpression) {
9✔
895
                    innerQuery.cancelOperandEdit();
1✔
896
                }
897

898
                innerQuery.expressionTree = this.getExpressionTreeCopy(this._editedExpression.expression.searchTree);
9✔
899
                this.innerQueryNewExpressionTree = null;
9✔
900
            }
901
        }
902

903
        if (this._editedExpression) {
461✔
904
            this._editedExpression.inEditMode = false;
30✔
905

906
            if (!this._editedExpression.expression.fieldName) {
30✔
907
                this.deleteItem(this._editedExpression);
16✔
908
            }
909

910
            this._editedExpression = null;
30✔
911
        }
912

913
        if (!this.expressionTree && this.contextualGroup) {
461✔
914
            this.initialOperator = this.contextualGroup.operator;
1✔
915
        }
916
    }
917

918
    /**
919
     * @hidden @internal
920
     */
921
    public operandCanBeCommitted(): boolean {
922
        const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
1,548✔
923

924
        return this.selectedField && this.selectedCondition &&
1,548✔
925
            (
926
                (
927
                    ((!Array.isArray(this.searchValue.value) && !!this.searchValue.value) || (Array.isArray(this.searchValue.value) && this.searchValue.value.length !== 0)) &&
928
                    !(this.selectedField?.filters?.condition(this.selectedCondition)?.isNestedQuery)
929
                ) ||
930
                (
931
                    this.selectedField?.filters?.condition(this.selectedCondition)?.isNestedQuery && innerQuery && !!innerQuery.expressionTree && innerQuery.selectedReturnFields?.length > 0
932
                ) ||
933
                this.selectedField.filters.condition(this.selectedCondition)?.isUnary
934
            );
935
    }
936

937
    /**
938
     * @hidden @internal
939
     */
940
    public canCommitCurrentState(): boolean {
941
        const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
36✔
942
        if (innerQuery) {
36✔
943
            return this.selectedReturnFields?.length > 0 && innerQuery.canCommitCurrentState();
8✔
944
        } else {
945
            return this.selectedReturnFields?.length > 0 &&
28✔
946
                (
947
                    (!this._editedExpression) || // no edited expr
948
                    (this._editedExpression && !this.selectedField) || // empty edited expr
949
                    (this._editedExpression && this.operandCanBeCommitted() === true) // valid edited expr
950
                );
951
        }
952
    }
953

954
    /**
955
     * @hidden @internal
956
     */
957
    public commitCurrentState(): void {
958
        const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
3✔
959
        if (innerQuery) {
3✔
960
            innerQuery.commitCurrentState();
1✔
961
        }
962

963
        if (this._editedExpression) {
3✔
964
            if (this.selectedField) {
3!
965
                this.commitOperandEdit();
3✔
966
            } else {
967
                this.deleteItem(this._editedExpression);
×
968
                this._editedExpression = null;
×
969
            }
970
        }
971
    }
972

973
    /**
974
     * @hidden @internal
975
     */
976
    public exitEditAddMode(shouldPreventInit = false) {
45✔
977
        if (!this._editedExpression) {
219✔
978
            return;
204✔
979
        }
980

981
        this.exitOperandEdit();
15✔
982
        this.cancelOperandAdd();
15✔
983

984
        if (shouldPreventInit) {
15✔
985
            this._preventInit = true;
7✔
986
        }
987
    }
988

989
    /**
990
     * @hidden @internal
991
     *
992
     * used by the grid
993
     */
994
    public exitOperandEdit() {
995
        if (!this._editedExpression) {
34✔
996
            return;
15✔
997
        }
998

999
        if (this.operandCanBeCommitted()) {
19✔
1000
            this.commitOperandEdit();
9✔
1001
        } else {
1002
            this.cancelOperandEdit();
10✔
1003
        }
1004
    }
1005

1006
    /**
1007
     * @hidden @internal
1008
     */
1009
    public isExpressionGroup(expression: ExpressionItem): boolean {
1010
        return expression instanceof ExpressionGroupItem;
8,287✔
1011
    }
1012

1013
    /**
1014
     * @hidden @internal
1015
     */
1016
    public onExpressionFocus(expressionItem: ExpressionOperandItem) {
1017
        if (this.prevFocusedExpression) {
41✔
1018
            this.prevFocusedExpression.focused = false;
4✔
1019
        }
1020
        expressionItem.focused = true;
41✔
1021
        this.prevFocusedExpression = expressionItem;
41✔
1022
    }
1023

1024
    /**
1025
     * @hidden @internal
1026
     */
1027
    public onExpressionBlur(event, expressionItem: ExpressionOperandItem) {
1028
        if (this._prevFocusedContainer && this._prevFocusedContainer !== event.target.closest('.igx-filter-tree__expression-item')) {
2!
1029
            expressionItem.focused = false;
×
1030
        }
1031
        this._prevFocusedContainer = event.target.closest('.igx-filter-tree__expression-item');
2✔
1032
    }
1033

1034
    /**
1035
     * @hidden @internal
1036
     */
1037
    public onChipRemove(expressionItem: ExpressionItem) {
1038
        this.exitEditAddMode();
5✔
1039
        this.deleteItem(expressionItem);
5✔
1040
    }
1041

1042
    /**
1043
     * @hidden @internal
1044
     */
1045
    public focusChipAfterDrag = (index: number) => {
235✔
1046
        this._lastFocusedChipIndex = index;
11✔
1047
        this.focusEditedExpressionChip();
11✔
1048
    }
1049
    /**
1050
     * @hidden @internal
1051
     */
1052
    public addExpressionBlur() {
1053
        if (this.prevFocusedExpression) {
×
1054
            this.prevFocusedExpression.focused = false;
×
1055
        }
1056
        if (this.addExpressionItemDropDown && !this.addExpressionItemDropDown.collapsed) {
×
1057
            this.addExpressionItemDropDown.close();
×
1058
        }
1059
    }
1060

1061
    /**
1062
     * @hidden @internal
1063
     */
1064
    public onChipClick(expressionItem: ExpressionOperandItem, chip: IgxChipComponent) {
1065
        this.enterExpressionEdit(expressionItem, chip);
55✔
1066
    }
1067

1068
    /**
1069
     * @hidden @internal
1070
     */
1071
    public enterExpressionEdit(expressionItem: ExpressionOperandItem, chip?: IgxChipComponent) {
1072
        this.exitEditAddMode(true);
123✔
1073
        this.cdr.detectChanges();
123✔
1074
        this._lastFocusedChipIndex = chip ? this.expressionsChips.toArray().findIndex(expr => expr === chip) : this._lastFocusedChipIndex;
123✔
1075
        this.enterEditMode(expressionItem);
123✔
1076
    }
1077

1078

1079
    /**
1080
     * @hidden @internal
1081
     */
1082
    public clickExpressionAdd(targetButton: HTMLElement, chip: IgxChipComponent) {
1083
        this.exitEditAddMode(true);
4✔
1084
        this.cdr.detectChanges();
4✔
1085
        this._lastFocusedChipIndex = this.expressionsChips.toArray().findIndex(expr => expr === chip);
22✔
1086
        this.openExpressionAddDialog(targetButton);
4✔
1087
    }
1088

1089
    /**
1090
     * @hidden @internal
1091
     */
1092
    public openExpressionAddDialog(targetButton: HTMLElement) {
1093
        this.addExpressionDropDownOverlaySettings.target = targetButton;
4✔
1094
        this.addExpressionDropDownOverlaySettings.positionStrategy = new ConnectedPositioningStrategy({
4✔
1095
            horizontalDirection: HorizontalAlignment.Right,
1096
            horizontalStartPoint: HorizontalAlignment.Left,
1097
            verticalStartPoint: VerticalAlignment.Bottom
1098
        });
1099

1100
        this.addExpressionItemDropDown.open(this.addExpressionDropDownOverlaySettings);
4✔
1101
    }
1102

1103
    /**
1104
     * @hidden @internal
1105
     */
1106
    public enterExpressionAdd(event: ISelectionEventArgs, expressionItem: ExpressionOperandItem) {
1107
        if (this._addModeExpression) {
1!
1108
            this._addModeExpression.inAddMode = false;
×
1109
        }
1110

1111
        if (this.parentExpression) {
1!
1112
            this.inEditModeChange.emit(this.parentExpression);
×
1113
        }
1114

1115
        const parent = expressionItem.parent ?? this.rootGroup;
1!
1116
        requestAnimationFrame(() => {
1✔
1117
            if (event.newSelection.value === 'addCondition') {
1!
1118
                this.addCondition(parent, expressionItem);
1✔
1119
            } else if (event.newSelection.value === 'addGroup') {
×
1120
                this.addReverseGroup(parent, expressionItem);
×
1121
            }
1122
            expressionItem.inAddMode = true;
1✔
1123
            this._addModeExpression = expressionItem;
1✔
1124
        })
1125
    }
1126

1127
    /**
1128
     * @hidden @internal
1129
     */
1130
    public enterEditMode(expressionItem: ExpressionOperandItem) {
1131
        if (this._editedExpression) {
123!
1132
            this._editedExpression.inEditMode = false;
×
1133
        }
1134

1135
        if (this.parentExpression) {
123✔
1136
            this.inEditModeChange.emit(this.parentExpression);
13✔
1137
        }
1138

1139
        expressionItem.hovered = false;
123✔
1140
        this.fields = this.selectedEntity ? this.selectedEntity.fields : null;
123✔
1141
        this.selectedField =
123✔
1142
            expressionItem.expression.fieldName ?
123✔
1143
                this.fields?.find(field => field.field === expressionItem.expression.fieldName)
89✔
1144
                : null;
1145
        this.selectedCondition =
123✔
1146
            expressionItem.expression.condition ?
123✔
1147
                expressionItem.expression.condition.name :
1148
                null;
1149
        this.searchValue.value = expressionItem.expression.searchVal instanceof Set ?
123✔
1150
            Array.from(expressionItem.expression.searchVal) :
1151
            expressionItem.expression.searchVal;
1152

1153
        expressionItem.inEditMode = true;
123✔
1154
        this._editedExpression = expressionItem;
123✔
1155
        this.cdr.detectChanges();
123✔
1156

1157
        this.entitySelectOverlaySettings.target = this.entitySelect.element;
123✔
1158
        this.entitySelectOverlaySettings.excludeFromOutsideClick = [this.entitySelect.element as HTMLElement];
123✔
1159
        this.entitySelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
123✔
1160

1161
        if (this.returnFieldSelect) {
123✔
1162
            this.returnFieldSelectOverlaySettings.target = this.returnFieldSelect.element;
13✔
1163
            this.returnFieldSelectOverlaySettings.excludeFromOutsideClick = [this.returnFieldSelect.element as HTMLElement];
13✔
1164
            this.returnFieldSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
13✔
1165
        }
1166
        if (this.fieldSelect) {
123✔
1167
            this.fieldSelectOverlaySettings.target = this.fieldSelect.element;
120✔
1168
            this.fieldSelectOverlaySettings.excludeFromOutsideClick = [this.fieldSelect.element as HTMLElement];
120✔
1169
            this.fieldSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
120✔
1170
        }
1171
        if (this.conditionSelect) {
123✔
1172
            this.conditionSelectOverlaySettings.target = this.conditionSelect.element;
120✔
1173
            this.conditionSelectOverlaySettings.excludeFromOutsideClick = [this.conditionSelect.element as HTMLElement];
120✔
1174
            this.conditionSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
120✔
1175
        }
1176

1177
        if (!this.selectedField) {
123✔
1178
            this.fieldSelect.input.nativeElement.focus();
68✔
1179
        } else if (this.selectedField.filters.condition(this.selectedCondition)?.isUnary) {
55✔
1180
            this.conditionSelect?.input.nativeElement.focus();
5✔
1181
        } else {
1182
            const input = this.searchValueInput?.nativeElement || this.picker?.getEditElement();
50✔
1183
            input?.focus();
50✔
1184
        }
1185

1186
        (this.editingInputs?.nativeElement.parentElement as HTMLElement)?.scrollIntoView({ block: "nearest", inline: "nearest" });
123✔
1187
    }
1188

1189
    /**
1190
     * @hidden @internal
1191
     */
1192
    public onConditionSelectChanging(event: ISelectionEventArgs) {
1193
        event.cancel = true;
61✔
1194
        this.selectedCondition = event.newSelection.value;
61✔
1195
        this.conditionSelect.close();
61✔
1196
        this.cdr.detectChanges();
61✔
1197
    }
1198

1199
    /**
1200
     * @hidden @internal
1201
     */
1202
    public onKeyDown(eventArgs: KeyboardEvent) {
1203
        eventArgs.stopPropagation();
×
1204
    }
1205

1206
    /**
1207
     * @hidden @internal
1208
     */
1209
    public onGroupClick(groupContextMenuDropDown: any, targetButton: HTMLButtonElement, groupItem: ExpressionGroupItem) {
1210
        this.exitEditAddMode();
7✔
1211
        this.cdr.detectChanges();
7✔
1212

1213
        this.groupContextMenuDropDown = groupContextMenuDropDown;
7✔
1214
        this.groupContextMenuDropDownOverlaySettings.target = targetButton;
7✔
1215
        this.groupContextMenuDropDownOverlaySettings.positionStrategy = new ConnectedPositioningStrategy({
7✔
1216
            horizontalDirection: HorizontalAlignment.Right,
1217
            horizontalStartPoint: HorizontalAlignment.Left,
1218
            verticalStartPoint: VerticalAlignment.Bottom
1219
        });
1220

1221
        if (groupContextMenuDropDown.collapsed) {
7!
1222
            this.contextualGroup = groupItem;
7✔
1223
            groupContextMenuDropDown.open(this.groupContextMenuDropDownOverlaySettings);
7✔
1224
        } else {
1225
            groupContextMenuDropDown.close();
×
1226
        }
1227
    }
1228

1229
    /**
1230
     * @hidden @internal
1231
     */
1232
    public getOperator(expressionItem: any) {
1233
        // if (!expressionItem && !this.expressionTree && !this.initialOperator) {
1234
        //     this.initialOperator = 0;
1235
        // }
1236

1237
        const operator = expressionItem ?
24,428✔
1238
            expressionItem.operator :
1239
            this.expressionTree ?
2,546✔
1240
                this.expressionTree.operator :
1241
                this.initialOperator;
1242
        return operator;
24,428✔
1243
    }
1244

1245
    /**
1246
     * @hidden @internal
1247
     */
1248
    public getSwitchGroupText(expressionItem: any) {
1249
        const operator = this.getOperator(expressionItem);
4,060✔
1250
        const condition = operator === FilteringLogic.Or ? this.resourceStrings.igx_query_builder_and_label : this.resourceStrings.igx_query_builder_or_label
4,060✔
1251
        return this.resourceStrings.igx_query_builder_switch_group.replace('{0}', condition.toUpperCase());
4,060✔
1252
    }
1253

1254
    /**
1255
     * @hidden @internal
1256
     */
1257
    public onGroupContextMenuDropDownSelectionChanging(event: ISelectionEventArgs) {
1258
        event.cancel = true;
4✔
1259

1260
        if (event.newSelection.value === 'switchCondition') {
4✔
1261
            const newOperator = (!this.expressionTree ? this.initialOperator : (this.contextualGroup ?? this._expressionTree).operator) === 0 ? 1 : 0;
3!
1262
            this.selectFilteringLogic(newOperator);
3✔
1263
        } else if (event.newSelection.value === 'ungroup') {
1✔
1264
            this.ungroup();
1✔
1265
        }
1266

1267
        this.groupContextMenuDropDown.close();
4✔
1268
    }
1269

1270
    /**
1271
     * @hidden @internal
1272
     */
1273
    public ungroup() {
1274
        const selectedGroup = this.contextualGroup;
1✔
1275
        const parent = selectedGroup.parent;
1✔
1276
        if (parent) {
1✔
1277
            const index = parent.children.indexOf(selectedGroup);
1✔
1278
            parent.children.splice(index, 1, ...selectedGroup.children);
1✔
1279

1280
            for (const expr of selectedGroup.children) {
1✔
1281
                expr.parent = parent;
2✔
1282
            }
1283
        }
1284
        this.commitOperandEdit();
1✔
1285
    }
1286

1287
    /**
1288
     * @hidden @internal
1289
     */
1290
    public selectFilteringLogic(index: number) {
1291
        if (!this.expressionTree) {
3!
1292
            this.initialOperator = index;
×
1293
            return;
×
1294
        }
1295

1296
        if (this.contextualGroup) {
3✔
1297
            this.contextualGroup.operator = index as FilteringLogic;
2✔
1298
            this.commitOperandEdit();
2✔
1299
        } else if (this.expressionTree) {
1✔
1300
            this._expressionTree.operator = index as FilteringLogic;
1✔
1301
        }
1302

1303
        this.initialOperator = null;
3✔
1304
    }
1305

1306
    /**
1307
     * @hidden @internal
1308
     */
1309
    public getConditionFriendlyName(name: string): string {
1310
        // 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.
1311
        // So instead of in/notIn we end up with 'inQuery'/'notInQuery', hence removing the suffix from the friendly name.
1312
        return this.resourceStrings[`igx_query_builder_filter_${name?.replace('Query', '')}`] || name;
33,306✔
1313
    }
1314

1315
    /**
1316
     * @hidden @internal
1317
     */
1318
    public isDate(value: any) {
1319
        return value instanceof Date;
4,550✔
1320
    }
1321

1322
    /**
1323
     * @hidden @internal
1324
     */
1325
    public invokeClick(eventArgs: KeyboardEvent) {
1326
        if (!this.dragService.dropGhostExpression && this.platform.isActivationKey(eventArgs)) {
2✔
1327
            eventArgs.preventDefault();
2✔
1328
            (eventArgs.currentTarget as HTMLElement).click();
2✔
1329
        }
1330
    }
1331

1332
    /**
1333
     * @hidden @internal
1334
     */
1335
    public openPicker(args: KeyboardEvent) {
1336
        if (this.platform.isActivationKey(args)) {
×
1337
            args.preventDefault();
×
1338
            this.picker.open();
×
1339
        }
1340
    }
1341

1342
    /**
1343
     * @hidden @internal
1344
     */
1345
    public onOutletPointerDown(event) {
1346
        // This prevents closing the select's dropdown when clicking the scroll
1347
        event.preventDefault();
57✔
1348
    }
1349

1350
    /**
1351
     * @hidden @internal
1352
     */
1353
    public getConditionList(): string[] {
1354
        if (!this.selectedField) return [];
1,502✔
1355

1356
        if (this.entities?.length === 1 && !this.entities[0].name) {
1,106✔
1357
            return this.selectedField.filters.conditionList();
214✔
1358
        }
1359

1360
        return this.selectedField.filters.extendedConditionList();
892✔
1361
    }
1362

1363
    /**
1364
     * @hidden @internal
1365
     */
1366
    public getFormatter(field: string) {
1367
        return this.fields?.find(el => el.field === field)?.formatter;
20,132✔
1368
    }
1369

1370
    /**
1371
     * @hidden @internal
1372
     */
1373
    public getFormat(field: string) {
1374
        return this.fields?.find(el => el.field === field).pipeArgs.format;
1,845✔
1375
    }
1376

1377
    /**
1378
     * @hidden @internal
1379
     *
1380
     * used by the grid
1381
     */
1382
    public setAddButtonFocus() {
1383
        if (this.addRootAndGroupButton) {
39!
1384
            this.addRootAndGroupButton.nativeElement.focus();
×
1385
        } else if (this.addConditionButton) {
39✔
1386
            this.addConditionButton.nativeElement.focus();
18✔
1387
        }
1388
    }
1389

1390
    /**
1391
     * @hidden @internal
1392
     */
1393
    public context(expression: ExpressionItem, afterExpression?: ExpressionItem) {
1394
        return {
14,112✔
1395
            $implicit: expression,
1396
            afterExpression
1397
        };
1398
    }
1399

1400
    public formatReturnFields(innerTree: IFilteringExpressionsTree) {
1401
        const returnFields = innerTree.returnFields;
808✔
1402
        let text = returnFields.join(', ');
808✔
1403
        const innerTreeEntity = this.entities?.find(el => el.name === innerTree.entity);
808✔
1404
        if (returnFields.length === innerTreeEntity?.fields.length) {
808!
1405
            text = this.resourceStrings.igx_query_builder_all_fields;
×
1406
        } else {
1407
            text = returnFields.join(', ');
808✔
1408
            text = text.length > 25 ? text.substring(0, 25) + ' ...' : text;
808!
1409
        }
1410
        return text;
808✔
1411
    }
1412

1413
    public isInEditMode(): boolean {
1414
        return !this.parentExpression || (this.parentExpression && this.parentExpression.inEditMode);
19,363✔
1415
    }
1416

1417
    public onInEditModeChanged(expressionItem: ExpressionOperandItem) {
1418
        if (!expressionItem.inEditMode) {
13!
1419
            this.enterExpressionEdit(expressionItem);
×
1420
        }
1421
    }
1422

1423
    public getExpressionTreeCopy(expressionTree: IExpressionTree, shouldAssignInnerQueryExprTree?: boolean): IExpressionTree {
1424
        if (!expressionTree) {
210✔
1425
            return null;
180✔
1426
        }
1427

1428
        const exprTreeCopy = new FilteringExpressionsTree(expressionTree.operator, expressionTree.fieldName, expressionTree.entity, expressionTree.returnFields);
30✔
1429
        exprTreeCopy.filteringOperands = [];
30✔
1430

1431
        expressionTree.filteringOperands.forEach(o => isTree(o) ? exprTreeCopy.filteringOperands.push(this.getExpressionTreeCopy(o)) : exprTreeCopy.filteringOperands.push(o));
55!
1432

1433
        if (!this.innerQueryNewExpressionTree && shouldAssignInnerQueryExprTree) {
30✔
1434
            this.innerQueryNewExpressionTree = exprTreeCopy;
18✔
1435
        }
1436

1437
        return exprTreeCopy;
30✔
1438
    }
1439

1440
    public onSelectAllClicked() {
1441
        if (
2✔
1442
            (this._selectedReturnFields.length > 0 && this._selectedReturnFields.length < this._selectedEntity.fields.length) ||
5✔
1443
            this._selectedReturnFields.length == this._selectedEntity.fields.length
1444
        ) {
1445
            this.returnFieldsCombo.deselectAllItems();
1✔
1446
        } else {
1447
            this.returnFieldsCombo.selectAllItems();
1✔
1448
        }
1449
    }
1450

1451
    public onReturnFieldSelectChanging(event: IComboSelectionChangingEventArgs | ISelectionEventArgs) {
1452
        let newSelection = [];
11✔
1453
        if (Array.isArray(event.newSelection)) {
11✔
1454
            newSelection = event.newSelection.map(item => item.field)
11✔
1455
        } else {
1456
            newSelection.push(event.newSelection.value);
6✔
1457
            this._selectedReturnFields = newSelection;
6✔
1458
        }
1459

1460
        this.initExpressionTree(this.selectedEntity.name, newSelection);
11✔
1461
    }
1462

1463
    public initExpressionTree(selectedEntityName: string, selectedReturnFields: string[]) {
1464
        if (!this._expressionTree) {
49✔
1465
            this._expressionTree = this.createExpressionTreeFromGroupItem(new ExpressionGroupItem(FilteringLogic.And, this.rootGroup), selectedEntityName, selectedReturnFields);
33✔
1466
        }
1467

1468
        if (!this.parentExpression) {
49✔
1469
            this.expressionTreeChange.emit(this._expressionTree);
38✔
1470
        }
1471
    }
1472

1473
    public getSearchValueTemplateContext(defaultSearchValueTemplate): any {
1474
        const ctx = {
1,502✔
1475
            $implicit: this.searchValue,
1476
            selectedField: this.selectedField,
1477
            selectedCondition: this.selectedCondition,
1478
            defaultSearchValueTemplate: defaultSearchValueTemplate
1479
        };
1480
        return ctx;
1,502✔
1481
    }
1482

1483
    private getPipeArgs(field: FieldType) {
1484
        let pipeArgs = {...field.pipeArgs};
2,636✔
1485
        if (!pipeArgs) {
2,636!
NEW
1486
            pipeArgs = { digitsInfo: DEFAULT_PIPE_DIGITS_INFO };
×
1487
        }
1488

1489
        if (!pipeArgs.format) {
2,636✔
1490
            pipeArgs.format = field.dataType === DataType.Time ?
2,636!
1491
                DEFAULT_PIPE_TIME_FORMAT : field.dataType === DataType.DateTime ?
2,636!
1492
                    DEFAULT_PIPE_DATE_TIME_FORMAT : DEFAULT_PIPE_DATE_FORMAT;
1493
        }
1494
        
1495
        return pipeArgs;
2,636✔
1496
    }
1497

1498
    private selectDefaultCondition() {
1499
        if (this.selectedField && this.selectedField.filters) {
153✔
1500
            this.selectedCondition = this.selectedField.filters.conditionList().indexOf('equals') >= 0 ? 'equals' : this.selectedField.filters.conditionList()[0];
117✔
1501
        }
1502
    }
1503

1504
    private getFilters(field: FieldType) {
1505
        if (!field.filters) {
2,636✔
1506
            switch (field.dataType) {
2,636!
1507
                case DataType.Boolean:
1508
                    return IgxBooleanFilteringOperand.instance();
659✔
1509
                case DataType.Number:
1510
                case DataType.Currency:
1511
                case DataType.Percent:
1512
                    return IgxNumberFilteringOperand.instance();
951✔
1513
                case DataType.Date:
1514
                    return IgxDateFilteringOperand.instance();
367✔
1515
                case DataType.Time:
NEW
1516
                    return IgxTimeFilteringOperand.instance();
×
1517
                case DataType.DateTime:
NEW
1518
                    return IgxDateTimeFilteringOperand.instance();
×
1519
                case DataType.String:
1520
                default:
1521
                    return IgxStringFilteringOperand.instance();
659✔
1522
            }
1523
        }
1524
    }
1525

1526

1527
    private addGroup(operator: FilteringLogic, parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
1528
        this.cancelOperandAdd();
3✔
1529

1530
        const groupItem = new ExpressionGroupItem(operator, parent);
3✔
1531

1532
        if (parent) {
3!
1533
            if (afterExpression) {
3!
1534
                const index = parent.children.indexOf(afterExpression);
×
1535
                parent.children.splice(index + 1, 0, groupItem);
×
1536
            } else {
1537
                parent.children.push(groupItem);
3✔
1538
            }
1539
        } else {
1540
            this.rootGroup = groupItem;
×
1541
        }
1542

1543
        this.addCondition(groupItem);
3✔
1544
        this.currentGroup = groupItem;
3✔
1545
    }
1546

1547
    private createExpressionGroupItem(expressionTree: IExpressionTree, parent?: ExpressionGroupItem, entityName?: string): ExpressionGroupItem {
1548
        let groupItem: ExpressionGroupItem;
1549
        if (expressionTree) {
732✔
1550
            groupItem = new ExpressionGroupItem(expressionTree.operator, parent);
582✔
1551
            if (!expressionTree.filteringOperands) {
582!
1552
                return groupItem;
×
1553
            }
1554

1555
            for (let i = 0; i < expressionTree.filteringOperands.length; i++) {
582✔
1556
                const expr = expressionTree.filteringOperands[i];
1,283✔
1557

1558
                if (isTree(expr)) {
1,283✔
1559
                    groupItem.children.push(this.createExpressionGroupItem(expr, groupItem, expressionTree.entity));
82✔
1560
                } else {
1561
                    const filteringExpr = expr as IFilteringExpression;
1,201✔
1562
                    const exprCopy: IFilteringExpression = {
1,201✔
1563
                        fieldName: filteringExpr.fieldName,
1564
                        condition: filteringExpr.condition,
1565
                        conditionName: filteringExpr.condition?.name || filteringExpr.conditionName,
1,201!
1566
                        searchVal: filteringExpr.searchVal,
1567
                        searchTree: filteringExpr.searchTree,
1568
                        ignoreCase: filteringExpr.ignoreCase
1569
                    };
1570
                    const operandItem = new ExpressionOperandItem(exprCopy, groupItem);
1,201✔
1571
                    const field = this.fields?.find(el => el.field === filteringExpr.fieldName);
1,856✔
1572
                    operandItem.fieldLabel = field?.label || field?.header || field?.field;
1,201✔
1573
                    if (this._expandedExpressions.filter(e => e.searchTree == operandItem.expression.searchTree).length > 0) {
1,201!
1574
                        operandItem.expanded = true;
×
1575
                    }
1576
                    groupItem.children.push(operandItem);
1,201✔
1577
                }
1578
            }
1579

1580

1581
            if (expressionTree.entity) {
582✔
1582
                entityName = expressionTree.entity;
525✔
1583
            }
1584
            const entity = this.entities?.find(el => el.name === entityName);
863✔
1585
            if (entity) {
582✔
1586
                this.fields = entity.fields;
525✔
1587
            }
1588

1589
            this._selectedEntity = this.entities?.find(el => el.name === entityName);
863✔
1590
            this._selectedReturnFields =
582✔
1591
                !expressionTree.returnFields || expressionTree.returnFields.includes('*') || expressionTree.returnFields.includes('All') || expressionTree.returnFields.length === 0
2,396✔
1592
                    ? this.fields?.map(f => f.field)
1,173✔
1593
                    : this.fields?.filter(f => expressionTree.returnFields.indexOf(f.field) >= 0).map(f => f.field);
1,321✔
1594
        }
1595
        return groupItem;
732✔
1596
    }
1597

1598
    private createExpressionTreeFromGroupItem(groupItem: ExpressionGroupItem, entity?: string, returnFields?: string[]): FilteringExpressionsTree {
1599
        if (!groupItem) {
149✔
1600
            return null;
4✔
1601
        }
1602

1603
        const expressionTree = new FilteringExpressionsTree(groupItem.operator, undefined, entity, returnFields);
145✔
1604

1605
        for (let i = 0; i < groupItem.children.length; i++) {
145✔
1606
            const item = groupItem.children[i];
199✔
1607

1608
            if (item instanceof ExpressionGroupItem) {
199✔
1609
                const subTree = this.createExpressionTreeFromGroupItem((item as ExpressionGroupItem), entity, returnFields);
19✔
1610
                expressionTree.filteringOperands.push(subTree);
19✔
1611
            } else {
1612
                expressionTree.filteringOperands.push((item as ExpressionOperandItem).expression);
180✔
1613
            }
1614
        }
1615

1616
        return expressionTree;
145✔
1617
    }
1618

1619
    private scrollElementIntoView(target: HTMLElement) {
1620
        const container = this.expressionsContainer.nativeElement;
536✔
1621
        const targetOffset = target.offsetTop - container.offsetTop;
536✔
1622
        const delta = 10;
536✔
1623

1624
        if (container.scrollTop + delta > targetOffset) {
536✔
1625
            container.scrollTop = targetOffset - delta;
322✔
1626
        } else if (container.scrollTop + container.clientHeight < targetOffset + target.offsetHeight + delta) {
214✔
1627
            container.scrollTop = targetOffset + target.offsetHeight + delta - container.clientHeight;
209✔
1628
        }
1629
    }
1630

1631
    private focusEditedExpressionChip() {
1632
        if (this._timeoutId) {
59✔
1633
            clearTimeout(this._timeoutId);
5✔
1634
        }
1635

1636
        this._timeoutId = setTimeout(() => {
59✔
1637
            if (this._lastFocusedChipIndex != -1) {
59✔
1638
                //Sort the expression chip list.
1639
                //If there was a recent drag&drop and the tree hasn't rerendered(child query), they will be unordered
1640
                const sortedChips = this.expressionsChips.toArray().sort(function (a, b) {
57✔
1641
                    if (a === b) return 0;
73!
1642
                    if (a.chipArea.nativeElement.compareDocumentPosition(b.chipArea.nativeElement) & 2) {
73✔
1643
                        // b comes before a
1644
                        return 1;
60✔
1645
                    }
1646
                    return -1;
13✔
1647
                });
1648
                const chipElement = sortedChips[this._lastFocusedChipIndex]?.nativeElement;
57✔
1649
                if (chipElement) {
57✔
1650
                    chipElement.focus();
49✔
1651
                }
1652
                this._lastFocusedChipIndex = -1;
57✔
1653
                this._focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
57✔
1654
            }
1655
        }, this._focusDelay);
1656
    }
1657

1658
    private init() {
1659
        this.cancelOperandAdd();
434✔
1660
        this.cancelOperandEdit();
434✔
1661

1662
        // Ignore values of certain properties for the comparison
1663
        const propsToIgnore = ['parent', 'hovered', 'ignoreCase', 'inEditMode', 'inAddMode'];
434✔
1664
        const propsReplacer = function replacer(key, value) {
434✔
1665
            if (propsToIgnore.indexOf(key) >= 0) {
17,643✔
1666
                return undefined;
2,338✔
1667
            } else {
1668
                return value;
15,305✔
1669
            }
1670
        };
1671

1672
        // Skip root being recreated if the same
1673
        const newRootGroup = this.createExpressionGroupItem(this.expressionTree);
434✔
1674
        if (JSON.stringify(this.rootGroup, propsReplacer) !== JSON.stringify(newRootGroup, propsReplacer)) {
434✔
1675
            this.rootGroup = this.createExpressionGroupItem(this.expressionTree);
211✔
1676
            this.currentGroup = this.rootGroup;
211✔
1677
        }
1678

1679
        if (this.rootGroup?.children?.length == 0) {
434✔
1680
            this.rootGroup = null;
40✔
1681
            this.currentGroup = null;
40✔
1682
        }
1683
    }
1684

1685
    /** rootGroup is recreated after clicking Apply, which sets new expressionTree and calls init()*/
1686
    protected trackExpressionItem = trackByIdentity;
235✔
1687
}
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