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

IgniteUI / igniteui-angular / 13498154117

24 Feb 2025 12:34PM UTC coverage: 91.664% (+0.05%) from 91.619%
13498154117

Pull #14647

github

web-flow
Merge b5c23f676 into 66906e39c
Pull Request #14647: feat(query-builder): support for nested queries and other improvements

13327 of 15595 branches covered (85.46%)

894 of 978 new or added lines in 19 files covered. (91.41%)

1 existing line in 1 file now uncovered.

26876 of 29320 relevant lines covered (91.66%)

33808.2 hits per line

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

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

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

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

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

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

114
    /**
115
     * Sets/gets the entities.
116
     */
117
    @Input()
118
    public entities: EntityType[];
119

120
    /**
121
     * Sets/gets the parent query builder component.
122
     */
123
    @Input()
124
    public queryBuilder: IgxQueryBuilderComponent;
125

126
    /**
127
     * Sets/gets the search value template.
128
     */
129
    @Input()
130
    public searchValueTemplate: TemplateRef<IgxQueryBuilderSearchValueTemplateDirective> = null;
207✔
131

132
    /**
133
    * Returns the parent expression operand.
134
    */
135
    @Input()
136
    public get parentExpression(): ExpressionOperandItem {
137
        return this._parentExpression;
52,460✔
138
    }
139

140
    /**
141
     * Sets the parent expression operand.
142
     */
143
    public set parentExpression(value: ExpressionOperandItem) {
144
        this._parentExpression = value;
71✔
145
    }
146

147
    /**
148
    * Returns the fields.
149
    */
150
    public get fields(): FieldType[] {
151
        if (!this._fields && this.isAdvancedFiltering()) {
22,815✔
152
            this._fields = this.entities[0].fields;
44✔
153
        }
154

155
        return this._fields;
22,815✔
156
    }
157

158
    /**
159
     * Sets the fields.
160
     */
161
    @Input()
162
    public set fields(fields: FieldType[]) {
163
        this._fields = fields;
620✔
164

165
        if (!this._fields && this.isAdvancedFiltering()) {
620✔
166
            this._fields = this.entities[0].fields;
27✔
167
        }
168

169
        if (this._fields) {
620✔
170
            this._fields.forEach(field => {
620✔
171
                this.setFilters(field);
2,555✔
172
                this.setFormat(field);
2,555✔
173
            });
174
        }
175
    }
176

177
    /**
178
    * Returns the expression tree.
179
    */
180
    public get expressionTree(): IExpressionTree {
181
        return this._expressionTree;
5,612✔
182
    }
183

184
    /**
185
     * Sets the expression tree.
186
     */
187
    @Input()
188
    public set expressionTree(expressionTree: IExpressionTree) {
189
        this._expressionTree = expressionTree;
409✔
190
        if (!expressionTree) {
409✔
191
            this._selectedEntity = null;
138✔
192
            this._selectedReturnFields = [];
138✔
193
        }
194

195
        if (!this._preventInit) {
409✔
196
            this.init();
400✔
197
        }
198
    }
199

200
    /**
201
     * Gets the `locale` of the query builder.
202
     * If not set, defaults to application's locale.
203
     */
204
    @Input()
205
    public get locale(): string {
206
        return this._locale;
882✔
207
    }
208

209
    /**
210
     * Sets the `locale` of the query builder.
211
     * Expects a valid BCP 47 language tag.
212
     */
213
    public set locale(value: string) {
214
        this._locale = value;
343✔
215
        // if value is invalid, set it back to _localeId
216
        try {
343✔
217
            getLocaleFirstDayOfWeek(this._locale);
343✔
218
        } catch {
219
            this._locale = this._localeId;
92✔
220
        }
221
    }
222

223
    /**
224
     * Sets the resource strings.
225
     * By default it uses EN resources.
226
     */
227
    @Input()
228
    public set resourceStrings(value: IQueryBuilderResourceStrings) {
229
        this._resourceStrings = Object.assign({}, this._resourceStrings, value);
137✔
230
    }
231

232
    /**
233
     * Returns the resource strings.
234
     */
235
    public get resourceStrings(): IQueryBuilderResourceStrings {
236
        return this._resourceStrings;
110,205✔
237
    }
238

239
    /**
240
     * Event fired as the expression tree is changed.
241
     */
242
    @Output()
243
    public expressionTreeChange = new EventEmitter<IExpressionTree>();
207✔
244

245
    /**
246
     * Event fired if a nested query builder tree is being edited.
247
     */
248
    @Output()
249
    public inEditModeChange = new EventEmitter<ExpressionOperandItem>();
207✔
250

251
    @ViewChild('entitySelect', { read: IgxSelectComponent })
252
    protected entitySelect: IgxSelectComponent;
253

254
    @ViewChild('editingInputs', { read: ElementRef })
255
    private editingInputs: ElementRef;
256

257
    @ViewChild('returnFieldsCombo', { read: IgxComboComponent })
258
    private returnFieldsCombo: IgxComboComponent;
259

260
    @ViewChild('returnFieldSelect', { read: IgxSelectComponent })
261
    protected returnFieldSelect: IgxSelectComponent;
262

263
    @ViewChild('fieldSelect', { read: IgxSelectComponent })
264
    private fieldSelect: IgxSelectComponent;
265

266
    @ViewChild('conditionSelect', { read: IgxSelectComponent })
267
    private conditionSelect: IgxSelectComponent;
268

269
    @ViewChild('searchValueInput', { read: ElementRef })
270
    private searchValueInput: ElementRef;
271

272
    @ViewChild('picker')
273
    private picker: IgxDatePickerComponent | IgxTimePickerComponent;
274

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

278
    @ViewChild('addConditionButton', { read: ElementRef })
279
    private addConditionButton: ElementRef;
280

281
    @ViewChild('entityChangeDialog', { read: IgxDialogComponent })
282
    private entityChangeDialog: IgxDialogComponent;
283

284
    @ViewChild('addOptionsDropDown', { read: IgxDropDownComponent })
285
    private addExpressionItemDropDown: IgxDropDownComponent;
286

287
    @ViewChild('groupContextMenuDropDown', { read: IgxDropDownComponent })
288
    private groupContextMenuDropDown: IgxDropDownComponent;
289

290
    @ViewChildren(IgxChipComponent, { read: IgxChipComponent })
291
    private expressionsChips: QueryList<IgxChipComponent>;
292

293
    /**
294
     * @hidden @internal
295
     */
296
    @ContentChild(IgxQueryBuilderHeaderComponent)
297
    public headerContent: IgxQueryBuilderHeaderComponent;
298

299
    @ViewChild('editingInputsContainer', { read: ElementRef })
300
    protected set editingInputsContainer(value: ElementRef) {
301
        if ((value && !this._editingInputsContainer) ||
589✔
302
            (value && this._editingInputsContainer && this._editingInputsContainer.nativeElement !== value.nativeElement)) {
303
            requestAnimationFrame(() => {
200✔
304
                this.scrollElementIntoView(value.nativeElement);
200✔
305
            });
306
        }
307

308
        this._editingInputsContainer = value;
589✔
309
    }
310

311
    /** @hidden */
312
    protected get editingInputsContainer(): ElementRef {
NEW
313
        return this._editingInputsContainer;
×
314
    }
315

316
    @ViewChild('currentGroupButtonsContainer', { read: ElementRef })
317
    protected set currentGroupButtonsContainer(value: ElementRef) {
318
        if ((value && !this._currentGroupButtonsContainer) ||
589✔
319
            (value && this._currentGroupButtonsContainer && this._currentGroupButtonsContainer.nativeElement !== value.nativeElement)) {
320
            requestAnimationFrame(() => {
278✔
321
                this.scrollElementIntoView(value.nativeElement);
278✔
322
            });
323
        }
324

325
        this._currentGroupButtonsContainer = value;
589✔
326
    }
327

328
    /** @hidden */
329
    protected get currentGroupButtonsContainer(): ElementRef {
NEW
330
        return this._currentGroupButtonsContainer;
×
331
    }
332

333
    @ViewChild('expressionsContainer')
334
    private expressionsContainer: ElementRef;
335

336
    @ViewChild('overlayOutlet', { read: IgxOverlayOutletDirective, static: true })
337
    private overlayOutlet: IgxOverlayOutletDirective;
338

339
    @ViewChildren(IgxQueryBuilderTreeComponent)
340
    private innerQueries: QueryList<IgxQueryBuilderTreeComponent>;
341

342
    /**
343
     * @hidden @internal
344
     */
345
    public innerQueryNewExpressionTree: IExpressionTree;
346

347
    /**
348
     * @hidden @internal
349
     */
350
    public rootGroup: ExpressionGroupItem;
351

352
    /**
353
     * @hidden @internal
354
     */
355
    public selectedExpressions: ExpressionOperandItem[] = [];
207✔
356

357
    /**
358
     * @hidden @internal
359
     */
360
    public currentGroup: ExpressionGroupItem;
361

362
    /**
363
     * @hidden @internal
364
     */
365
    public contextualGroup: ExpressionGroupItem;
366

367
    /**
368
     * @hidden @internal
369
     */
370
    public filteringLogics;
371

372
    /**
373
     * @hidden @internal
374
     */
375
    public selectedCondition: string;
376

377
    /**
378
     * @hidden @internal
379
     */
380
    public searchValue: { value: any } = { value: null };
207✔
381

382
    /**
383
     * @hidden @internal
384
     */
385
    public pickerOutlet: IgxOverlayOutletDirective | ElementRef;
386

387
    /**
388
     * @hidden @internal
389
     */
390
    public prevFocusedExpression: ExpressionOperandItem;
391

392
    /**
393
     * @hidden @internal
394
     */
395
    public initialOperator = 0;
207✔
396

397
    /**
398
     * @hidden @internal
399
     */
400
    public returnFieldSelectOverlaySettings: OverlaySettings = {
207✔
401
        scrollStrategy: new AbsoluteScrollStrategy(),
402
        modal: false,
403
        closeOnOutsideClick: true
404
    };
405

406
    /**
407
     * @hidden @internal
408
     */
409
    public entitySelectOverlaySettings: OverlaySettings = {
207✔
410
        scrollStrategy: new AbsoluteScrollStrategy(),
411
        modal: false,
412
        closeOnOutsideClick: true
413
    };
414

415
    /**
416
     * @hidden @internal
417
     */
418
    public fieldSelectOverlaySettings: OverlaySettings = {
207✔
419
        scrollStrategy: new AbsoluteScrollStrategy(),
420
        modal: false,
421
        closeOnOutsideClick: true
422
    };
423

424
    /**
425
     * @hidden @internal
426
     */
427
    public conditionSelectOverlaySettings: OverlaySettings = {
207✔
428
        scrollStrategy: new AbsoluteScrollStrategy(),
429
        modal: false,
430
        closeOnOutsideClick: true
431
    };
432

433
    /**
434
     * @hidden @internal
435
     */
436
    public addExpressionDropDownOverlaySettings: OverlaySettings = {
207✔
437
        scrollStrategy: new AbsoluteScrollStrategy(),
438
        modal: false,
439
        closeOnOutsideClick: true
440
    };
441

442
    /**
443
     * @hidden @internal
444
     */
445
    public groupContextMenuDropDownOverlaySettings: OverlaySettings = {
207✔
446
        scrollStrategy: new AbsoluteScrollStrategy(),
447
        modal: false,
448
        closeOnOutsideClick: true
449
    };
450

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

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

477
        return !this.parentExpression && this.selectedEntity ? this.queryBuilder.disableEntityChange : false;
4,946✔
478
    }
479

480
    /**
481
     * Returns the current level.
482
     */
483
    public get level(): number {
484
        let parent = this.elRef.nativeElement.parentElement;
4,289✔
485
        let _level = 0;
4,289✔
486
        while (parent) {
4,289✔
487
            if (parent.localName === 'igx-query-builder-tree') {
31,024✔
488
                _level++;
1,582✔
489
            }
490
            parent = parent.parentElement;
31,024✔
491
        }
492
        return _level;
4,289✔
493
    }
494

495
    private _positionSettings = {
207✔
496
        horizontalStartPoint: HorizontalAlignment.Right,
497
        verticalStartPoint: VerticalAlignment.Top
498
    };
499

500
    private _overlaySettings: OverlaySettings = {
207✔
501
        closeOnOutsideClick: false,
502
        modal: false,
503
        positionStrategy: new ConnectedPositioningStrategy(this._positionSettings),
504
        scrollStrategy: new CloseScrollStrategy()
505
    };
506

507
    /** @hidden */
508
    protected isAdvancedFiltering(): boolean {
509
        return this.entities?.length === 1 && !this.entities[0]?.name;
8,995✔
510
    }
511

512
    /** @hidden */
513
    protected isSearchValueInputDisabled(): boolean {
514
        return !this.selectedField ||
1,477✔
515
            !this.selectedCondition ||
516
            (this.selectedField &&
517
                (this.selectedField.filters.condition(this.selectedCondition).isUnary ||
518
                    this.selectedField.filters.condition(this.selectedCondition).isNestedQuery));
519
    }
520

521
    constructor(public cdr: ChangeDetectorRef,
207✔
522
        protected platform: PlatformUtil,
207✔
523
        protected el: ElementRef,
207✔
524
        private elRef: ElementRef,
207✔
525
        @Inject(LOCALE_ID) protected _localeId: string) {
207✔
526
        this.locale = this.locale || this._localeId;
207✔
527
    }
528

529
    /**
530
     * @hidden @internal
531
     */
532
    public ngAfterViewInit(): void {
533
        this._overlaySettings.outlet = this.overlayOutlet;
207✔
534
        this.entitySelectOverlaySettings.outlet = this.overlayOutlet;
207✔
535
        this.fieldSelectOverlaySettings.outlet = this.overlayOutlet;
207✔
536
        this.conditionSelectOverlaySettings.outlet = this.overlayOutlet;
207✔
537
        this.returnFieldSelectOverlaySettings.outlet = this.overlayOutlet;
207✔
538
        this.addExpressionDropDownOverlaySettings.outlet = this.overlayOutlet;
207✔
539
        this.groupContextMenuDropDownOverlaySettings.outlet = this.overlayOutlet;
207✔
540
        // Trigger additional change detection cycle
541
        this.cdr.detectChanges();
207✔
542
    }
543

544
    /**
545
     * @hidden @internal
546
     */
547
    public ngOnDestroy(): void {
548
        this.destroy$.next(true);
207✔
549
        this.destroy$.complete();
207✔
550
    }
551

552
    /**
553
     * @hidden @internal
554
     */
555
    public set selectedEntity(value: string) {
NEW
556
        this._selectedEntity = this.entities?.find(el => el.name === value);
×
557
    }
558

559
    /**
560
     * @hidden @internal
561
     */
562
    public get selectedEntity(): EntityType {
563
        return this._selectedEntity;
34,353✔
564
    }
565

566
    /**
567
     * @hidden @internal
568
     */
569
    public onEntitySelectChanging(event: ISelectionEventArgs) {
570
        event.cancel = true;
41✔
571
        this._entityNewValue = event.newSelection.value;
41✔
572
        if (event.oldSelection.value && this.queryBuilder.showEntityChangeDialog) {
41✔
573
            this.entityChangeDialog.open();
6✔
574
        } else {
575
            this.onEntityChangeConfirm();
35✔
576
        }
577
    }
578

579
    /**
580
     * @hidden
581
     */
582
    public onShowEntityChangeDialogChange(eventArgs: IChangeCheckboxEventArgs) {
583
        this.queryBuilder.showEntityChangeDialog = !eventArgs.checked;
1✔
584
    }
585

586
    /**
587
     * @hidden
588
     */
589
    public onEntityChangeCancel() {
590
        this.entityChangeDialog.close();
3✔
591
        this.entitySelect.close();
3✔
592
        this._entityNewValue = null;
3✔
593
    }
594

595
    /**
596
     * @hidden
597
     */
598
    public onEntityChangeConfirm() {
599
        if (this._parentExpression) {
37✔
600
            this._expressionTree = this.createExpressionTreeFromGroupItem(this.createExpressionGroupItem(this._expressionTree));
5✔
601
        }
602

603
        this._selectedEntity = this._entityNewValue;
37✔
604
        if (!this._selectedEntity.fields) {
37!
NEW
605
            this._selectedEntity.fields = [];
×
606
        }
607
        this.fields = this._entityNewValue ? this._entityNewValue.fields : [];
37!
608

609
        this._selectedReturnFields = this.parentExpression ? [] : this._entityNewValue.fields?.map(f => f.field);
128✔
610

611
        if (this._expressionTree) {
37✔
612
            this._expressionTree.entity = this._entityNewValue.name;
4✔
613
            this._expressionTree.returnFields = [];
4✔
614
            this._expressionTree.filteringOperands = [];
4✔
615

616
            this._editedExpression = null;
4✔
617
            if (!this.parentExpression) {
4✔
618
                this.expressionTreeChange.emit(this._expressionTree);
3✔
619
            }
620

621
            this.rootGroup = null;
4✔
622
            this.currentGroup = this.rootGroup;
4✔
623
        }
624

625
        this._selectedField = null;
37✔
626
        this.selectedCondition = null;
37✔
627
        this.searchValue.value = null;
37✔
628

629
        this.entityChangeDialog.close();
37✔
630
        this.entitySelect.close();
37✔
631

632
        this._entityNewValue = null;
37✔
633
        this.innerQueryNewExpressionTree = null;
37✔
634

635
        this.initExpressionTree(this._selectedEntity.name, this.selectedReturnFields);
37✔
636
    }
637

638
    /**
639
     * @hidden @internal
640
     */
641
    public set selectedReturnFields(value: string[]) {
642
        if (this._selectedReturnFields !== value) {
2✔
643
            this._selectedReturnFields = value;
2✔
644

645
            if (this._expressionTree && !this.parentExpression) {
2✔
646
                this._expressionTree.returnFields = value;
2✔
647
                this.expressionTreeChange.emit(this._expressionTree);
2✔
648
            }
649
        }
650
    }
651

652
    /**
653
     * @hidden @internal
654
     */
655
    public get selectedReturnFields(): string[] {
656
        if (typeof this._selectedReturnFields == 'string') {
21,337!
NEW
657
            return [this._selectedReturnFields];
×
658
        }
659
        return this._selectedReturnFields;
21,337✔
660
    }
661

662
    /**
663
     * @hidden @internal
664
     */
665
    public set selectedField(value: FieldType) {
666
        const oldValue = this._selectedField;
184✔
667

668
        if (this._selectedField !== value) {
184✔
669
            this._selectedField = value;
142✔
670
            this.selectDefaultCondition();
142✔
671
            if (oldValue && this._selectedField && this._selectedField.dataType !== oldValue.dataType) {
142✔
672
                this.searchValue.value = null;
17✔
673
                this.cdr.detectChanges();
17✔
674
            }
675
        }
676
    }
677

678
    /**
679
     * @hidden @internal
680
     */
681
    public get selectedField(): FieldType {
682
        return this._selectedField;
43,408✔
683
    }
684

685
    /**
686
     * @hidden @internal
687
     *
688
     * used by the grid
689
     */
690
    public setPickerOutlet(outlet?: IgxOverlayOutletDirective | ElementRef) {
691
        this.pickerOutlet = outlet;
44✔
692
    }
693

694
    /**
695
     * @hidden @internal
696
     *
697
     * used by the grid
698
     */
699
    public get isContextMenuVisible(): boolean {
NEW
700
        return !this.groupContextMenuDropDown.collapsed;
×
701
    }
702

703
    /**
704
     * @hidden @internal
705
     */
706
    public get hasEditedExpression(): boolean {
707
        return this._editedExpression !== undefined && this._editedExpression !== null;
10,145✔
708
    }
709

710
    /**
711
     * @hidden @internal
712
     */
713
    public addCondition(parent: ExpressionGroupItem, afterExpression?: ExpressionOperandItem, isUIInteraction?: boolean) {
714
        this.cancelOperandAdd();
67✔
715

716
        const operandItem = new ExpressionOperandItem({
67✔
717
            fieldName: null,
718
            condition: null,
719
            conditionName: null,
720
            ignoreCase: true,
721
            searchVal: null
722
        }, parent);
723

724
        const groupItem = new ExpressionGroupItem(this.getOperator(null) ?? FilteringLogic.And, parent);
67!
725
        this.contextualGroup = groupItem;
67✔
726
        this.initialOperator = null;
67✔
727

728
        this._lastFocusedChipIndex = this._lastFocusedChipIndex === undefined ? -1 : this._lastFocusedChipIndex;
67✔
729

730
        if (parent) {
67✔
731
            if (afterExpression) {
11✔
732
                const index = parent.children.indexOf(afterExpression);
1✔
733
                parent.children.splice(index + 1, 0, operandItem);
1✔
734
            } else {
735
                parent.children.push(operandItem);
10✔
736
            }
737
            this._lastFocusedChipIndex++;
11✔
738
        } else {
739
            this.rootGroup = groupItem;
56✔
740
            operandItem.parent = groupItem;
56✔
741
            this.rootGroup.children.push(operandItem);
56✔
742
            this._lastFocusedChipIndex = 0;
56✔
743
        }
744

745
        this._focusDelay = 250;
67✔
746

747
        if (isUIInteraction && !afterExpression) {
67✔
748
            this._lastFocusedChipIndex = this.expressionsChips.length;
63✔
749
            this._focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
63✔
750
        }
751

752
        this.enterExpressionEdit(operandItem);
67✔
753
    }
754

755
    /**
756
     * @hidden @internal
757
     */
758
    public addReverseGroup(parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
759
        parent = parent ?? this.rootGroup;
3!
760

761
        if (parent.operator === FilteringLogic.And) {
3✔
762
            this.addGroup(FilteringLogic.Or, parent, afterExpression);
2✔
763
        } else {
764
            this.addGroup(FilteringLogic.And, parent, afterExpression);
1✔
765
        }
766
    }
767

768
    /**
769
     * @hidden @internal
770
     */
771
    public endGroup(groupItem: ExpressionGroupItem) {
NEW
772
        this.currentGroup = groupItem.parent;
×
773
    }
774

775
    /**
776
     * @hidden @internal
777
     */
778
    public commitExpression() {
779
        this.commitOperandEdit();
44✔
780
        this.focusEditedExpressionChip();
44✔
781
    }
782

783
    /**
784
     * @hidden @internal
785
     */
786
    public discardExpression(expressionItem?: ExpressionOperandItem) {
787
        this.cancelOperandEdit();
11✔
788
        if (expressionItem && expressionItem.expression.fieldName) {
11✔
789
            this.focusEditedExpressionChip();
4✔
790
        }
791
    }
792

793
    /**
794
     * @hidden @internal
795
     */
796
    public commitOperandEdit() {
797
        const actualSearchValue = this.searchValue.value;
59✔
798
        if (this._editedExpression) {
59✔
799
            this._editedExpression.expression.fieldName = this.selectedField.field;
56✔
800
            this._editedExpression.expression.condition = this.selectedField.filters.condition(this.selectedCondition);
56✔
801
            this._editedExpression.expression.conditionName = this.selectedCondition;
56✔
802
            this._editedExpression.expression.searchVal = DataUtil.parseValue(this.selectedField.dataType, actualSearchValue) || actualSearchValue;
56✔
803
            this._editedExpression.fieldLabel = this.selectedField.label
56!
804
                ? this.selectedField.label
805
                : this.selectedField.header
56✔
806
                    ? this.selectedField.header
807
                    : this.selectedField.field;
808

809
            const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0]
56✔
810
            if (innerQuery && this.selectedField?.filters?.condition(this.selectedCondition)?.isNestedQuery) {
56✔
811
                innerQuery.exitEditAddMode();
7✔
812
                this._editedExpression.expression.searchTree = this.getExpressionTreeCopy(innerQuery.expressionTree);
7✔
813
                this._editedExpression.expression.searchTree.returnFields = innerQuery.selectedReturnFields;
7✔
814
            } else {
815
                this._editedExpression.expression.searchTree = null;
49✔
816
            }
817
            this.innerQueryNewExpressionTree = null;
56✔
818

819
            if (this.selectedField.filters.condition(this.selectedCondition)?.isUnary || this.selectedField.filters.condition(this.selectedCondition)?.isNestedQuery) {
56✔
820
                this._editedExpression.expression.searchVal = null;
12✔
821
            }
822

823
            this._editedExpression.inEditMode = false;
56✔
824
            this._editedExpression = null;
56✔
825
        }
826

827
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup, this.selectedEntity?.name, this.selectedReturnFields);
59✔
828
        if (!this.parentExpression) {
59✔
829
            this.expressionTreeChange.emit(this._expressionTree);
50✔
830
        }
831
    }
832

833
    /**
834
     * @hidden @internal
835
     */
836
    public cancelOperandAdd() {
837
        if (this._addModeExpression) {
485!
NEW
838
            this._addModeExpression.inAddMode = false;
×
NEW
839
            this._addModeExpression = null;
×
840
        }
841
    }
842

843
    private deleteItem = (expressionItem: ExpressionItem, skipEmit: boolean = false) => {
207✔
844
        if (!expressionItem.parent) {
47✔
845
            this.rootGroup = null;
15✔
846
            this.currentGroup = null;
15✔
847
            //this._expressionTree = null;
848
            return;
15✔
849
        }
850

851
        if (expressionItem === this.currentGroup) {
32!
NEW
852
            this.currentGroup = this.currentGroup.parent;
×
853
        }
854

855
        const children = expressionItem.parent.children;
32✔
856
        const index = children.indexOf(expressionItem);
32✔
857
        children.splice(index, 1);
32✔
858
        const entity = this.expressionTree ? this.expressionTree.entity : null;
32✔
859
        const returnFields = this.expressionTree ? this.expressionTree.returnFields : null;
32✔
860
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup, entity, returnFields); // TODO: don't recreate if not necessary
32✔
861

862
        if (!children.length) {
32✔
863
            this.deleteItem(expressionItem.parent, true);
16✔
864
        }
865

866
        if (!this.parentExpression && !skipEmit) {
32✔
867
            this.expressionTreeChange.emit(this._expressionTree);
30✔
868
        }
869
    }
870

871
    /**
872
     * @hidden @internal
873
     */
874
    public cancelOperandEdit() {
875
        if (this.innerQueries) {
427✔
876
            const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
220✔
877
            if (innerQuery) {
220✔
878
                if (innerQuery._editedExpression) {
9✔
879
                    innerQuery.cancelOperandEdit();
1✔
880
                }
881

882
                innerQuery.expressionTree = this.getExpressionTreeCopy(this._editedExpression.expression.searchTree);
9✔
883
                this.innerQueryNewExpressionTree = null;
9✔
884
            }
885
        }
886

887
        if (this._editedExpression) {
427✔
888
            this._editedExpression.inEditMode = false;
30✔
889

890
            if (!this._editedExpression.expression.fieldName) {
30✔
891
                this.deleteItem(this._editedExpression);
16✔
892
            }
893

894
            this._editedExpression = null;
30✔
895
        }
896

897
        if (!this.expressionTree && this.contextualGroup) {
427✔
898
            this.initialOperator = this.contextualGroup.operator;
1✔
899
        }
900
    }
901

902
    /**
903
     * @hidden @internal
904
     */
905
    public operandCanBeCommitted(): boolean {
906
        const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
1,540✔
907

908
        return this.selectedField && this.selectedCondition &&
1,540✔
909
            (
910
                (
911
                    ((!Array.isArray(this.searchValue.value) && !!this.searchValue.value) || (Array.isArray(this.searchValue.value) && this.searchValue.value.length !== 0)) &&
912
                    !(this.selectedField?.filters?.condition(this.selectedCondition)?.isNestedQuery)
913
                ) ||
914
                (
915
                    this.selectedField?.filters?.condition(this.selectedCondition)?.isNestedQuery && innerQuery && !!innerQuery.expressionTree && innerQuery.selectedReturnFields?.length > 0
916
                ) ||
917
                this.selectedField.filters.condition(this.selectedCondition)?.isUnary
918
            );
919
    }
920

921
    /**
922
     * @hidden @internal
923
     */
924
    public canCommitCurrentState(): boolean {
925
        const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
36✔
926
        if (innerQuery) {
36✔
927
            return this.selectedReturnFields?.length > 0 && innerQuery.canCommitCurrentState();
8✔
928
        } else {
929
            return this.selectedReturnFields?.length > 0 &&
28✔
930
                (
931
                    (!this._editedExpression) || // no edited expr
932
                    (this._editedExpression && !this.selectedField) || // empty edited expr
933
                    (this._editedExpression && this.operandCanBeCommitted() === true) // valid edited expr
934
                );
935
        }
936
    }
937

938
    /**
939
     * @hidden @internal
940
     */
941
    public commitCurrentState(): void {
942
        const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
3✔
943
        if (innerQuery) {
3✔
944
            innerQuery.commitCurrentState();
1✔
945
        }
946

947
        if (this._editedExpression) {
3✔
948
            if (this.selectedField) {
3!
949
                this.commitOperandEdit();
3✔
950
            } else {
NEW
951
                this.deleteItem(this._editedExpression);
×
NEW
952
                this._editedExpression = null;
×
953
            }
954
        }
955
    }
956

957
    /**
958
     * @hidden @internal
959
     */
960
    public exitEditAddMode(shouldPreventInit = false) {
38✔
961
        if (!this._editedExpression) {
210✔
962
            return;
195✔
963
        }
964

965
        this.exitOperandEdit();
15✔
966
        this.cancelOperandAdd();
15✔
967

968
        if (shouldPreventInit) {
15✔
969
            this._preventInit = true;
7✔
970
        }
971
    }
972

973
    /**
974
     * @hidden @internal
975
     *
976
     * used by the grid
977
     */
978
    public exitOperandEdit() {
979
        if (!this._editedExpression) {
34✔
980
            return;
15✔
981
        }
982

983
        if (this.operandCanBeCommitted()) {
19✔
984
            this.commitOperandEdit();
9✔
985
        } else {
986
            this.cancelOperandEdit();
10✔
987
        }
988
    }
989

990
    /**
991
     * @hidden @internal
992
     */
993
    public isExpressionGroup(expression: ExpressionItem): boolean {
994
        return expression instanceof ExpressionGroupItem;
11,372✔
995
    }
996

997
    /**
998
     * @hidden @internal
999
     */
1000
    public onExpressionFocus(expressionItem: ExpressionOperandItem) {
1001
        if (this.prevFocusedExpression) {
38✔
1002
            this.prevFocusedExpression.focused = false;
3✔
1003
        }
1004
        expressionItem.focused = true;
38✔
1005
        this.prevFocusedExpression = expressionItem;
38✔
1006
    }
1007

1008
    /**
1009
     * @hidden @internal
1010
     */
1011
    public onExpressionBlur(event, expressionItem: ExpressionOperandItem) {
1012
        if (this._prevFocusedContainer && this._prevFocusedContainer !== event.target.closest('.igx-filter-tree__expression-item')) {
3!
NEW
1013
            expressionItem.focused = false;
×
1014
        }
1015
        this._prevFocusedContainer = event.target.closest('.igx-filter-tree__expression-item');
3✔
1016
    }
1017

1018
    /**
1019
     * @hidden @internal
1020
     */
1021
    public onChipRemove(expressionItem: ExpressionItem) {
1022
        this.exitEditAddMode();
5✔
1023
        this.deleteItem(expressionItem);
5✔
1024
    }
1025

1026
    private focusChipAfterDrag = (index: number) => {
207✔
1027
        this._lastFocusedChipIndex = index;
10✔
1028
        this.focusEditedExpressionChip();
10✔
1029
    }
1030

1031
    /**
1032
     * @hidden @internal
1033
     */
1034
    public dragService: IgxQueryBuilderDragService = new IgxQueryBuilderDragService(this, this.el, this.deleteItem, this.focusChipAfterDrag);
207✔
1035

1036
    /**
1037
     * @hidden @internal
1038
     */
1039
    public addExpressionBlur() {
NEW
1040
        if (this.prevFocusedExpression) {
×
NEW
1041
            this.prevFocusedExpression.focused = false;
×
1042
        }
NEW
1043
        if (this.addExpressionItemDropDown && !this.addExpressionItemDropDown.collapsed) {
×
NEW
1044
            this.addExpressionItemDropDown.close();
×
1045
        }
1046
    }
1047

1048
    /**
1049
     * @hidden @internal
1050
     */
1051
    public onChipClick(expressionItem: ExpressionOperandItem, chip: IgxChipComponent) {
1052
        this.enterExpressionEdit(expressionItem, chip);
55✔
1053
    }
1054

1055
    /**
1056
     * @hidden @internal
1057
     */
1058
    public enterExpressionEdit(expressionItem: ExpressionOperandItem, chip?: IgxChipComponent) {
1059
        this.exitEditAddMode(true);
122✔
1060
        this.cdr.detectChanges();
122✔
1061
        this._lastFocusedChipIndex = chip ? this.expressionsChips.toArray().findIndex(expr => expr === chip) : this._lastFocusedChipIndex;
122✔
1062
        this.enterEditMode(expressionItem);
122✔
1063
    }
1064

1065

1066
    /**
1067
     * @hidden @internal
1068
     */
1069
    public clickExpressionAdd(targetButton: HTMLElement, chip: IgxChipComponent) {
1070
        this.exitEditAddMode(true);
4✔
1071
        this.cdr.detectChanges();
4✔
1072
        this._lastFocusedChipIndex = this.expressionsChips.toArray().findIndex(expr => expr === chip);
22✔
1073
        this.openExpressionAddDialog(targetButton);
4✔
1074
    }
1075

1076
    /**
1077
     * @hidden @internal
1078
     */
1079
    public openExpressionAddDialog(targetButton: HTMLElement) {
1080
        this.addExpressionDropDownOverlaySettings.target = targetButton;
4✔
1081
        this.addExpressionDropDownOverlaySettings.positionStrategy = new ConnectedPositioningStrategy({
4✔
1082
            horizontalDirection: HorizontalAlignment.Right,
1083
            horizontalStartPoint: HorizontalAlignment.Left,
1084
            verticalStartPoint: VerticalAlignment.Bottom
1085
        });
1086

1087
        this.addExpressionItemDropDown.open(this.addExpressionDropDownOverlaySettings);
4✔
1088
    }
1089

1090
    /**
1091
     * @hidden @internal
1092
     */
1093
    public enterExpressionAdd(event: ISelectionEventArgs, expressionItem: ExpressionOperandItem) {
1094
        if (this._addModeExpression) {
1!
NEW
1095
            this._addModeExpression.inAddMode = false;
×
1096
        }
1097

1098
        if (this.parentExpression) {
1!
NEW
1099
            this.inEditModeChange.emit(this.parentExpression);
×
1100
        }
1101

1102
        const parent = expressionItem.parent ?? this.rootGroup;
1!
1103
        requestAnimationFrame(() => {
1✔
1104
            if (event.newSelection.value === 'addCondition') {
1!
1105
                this.addCondition(parent, expressionItem);
1✔
NEW
1106
            } else if (event.newSelection.value === 'addGroup') {
×
NEW
1107
                this.addReverseGroup(parent, expressionItem);
×
1108
            }
1109
            expressionItem.inAddMode = true;
1✔
1110
            this._addModeExpression = expressionItem;
1✔
1111
        })
1112
    }
1113

1114
    /**
1115
     * @hidden @internal
1116
     */
1117
    public enterEditMode(expressionItem: ExpressionOperandItem) {
1118
        if (this._editedExpression) {
122!
NEW
1119
            this._editedExpression.inEditMode = false;
×
1120
        }
1121

1122
        if (this.parentExpression) {
122✔
1123
            this.inEditModeChange.emit(this.parentExpression);
13✔
1124
        }
1125

1126
        expressionItem.hovered = false;
122✔
1127
        this.fields = this.selectedEntity ? this.selectedEntity.fields : null;
122✔
1128
        this.selectedField =
122✔
1129
            expressionItem.expression.fieldName ?
122✔
1130
                this.fields?.find(field => field.field === expressionItem.expression.fieldName)
89✔
1131
                : null;
1132
        this.selectedCondition =
122✔
1133
            expressionItem.expression.condition ?
122✔
1134
                expressionItem.expression.condition.name :
1135
                null;
1136
        this.searchValue.value = expressionItem.expression.searchVal instanceof Set ?
122✔
1137
                                    Array.from(expressionItem.expression.searchVal) :
1138
                                    expressionItem.expression.searchVal;
1139

1140
        expressionItem.inEditMode = true;
122✔
1141
        this._editedExpression = expressionItem;
122✔
1142
        this.cdr.detectChanges();
122✔
1143

1144
        this.entitySelectOverlaySettings.target = this.entitySelect.element;
122✔
1145
        this.entitySelectOverlaySettings.excludeFromOutsideClick = [this.entitySelect.element as HTMLElement];
122✔
1146
        this.entitySelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
122✔
1147

1148
        if (this.returnFieldSelect) {
122✔
1149
            this.returnFieldSelectOverlaySettings.target = this.returnFieldSelect.element;
13✔
1150
            this.returnFieldSelectOverlaySettings.excludeFromOutsideClick = [this.returnFieldSelect.element as HTMLElement];
13✔
1151
            this.returnFieldSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
13✔
1152
        }
1153
        if (this.fieldSelect) {
122✔
1154
            this.fieldSelectOverlaySettings.target = this.fieldSelect.element;
119✔
1155
            this.fieldSelectOverlaySettings.excludeFromOutsideClick = [this.fieldSelect.element as HTMLElement];
119✔
1156
            this.fieldSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
119✔
1157
        }
1158
        if (this.conditionSelect) {
122✔
1159
            this.conditionSelectOverlaySettings.target = this.conditionSelect.element;
119✔
1160
            this.conditionSelectOverlaySettings.excludeFromOutsideClick = [this.conditionSelect.element as HTMLElement];
119✔
1161
            this.conditionSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
119✔
1162
        }
1163

1164
        if (!this.selectedField) {
122✔
1165
            this.fieldSelect.input.nativeElement.focus();
67✔
1166
        } else if (this.selectedField.filters.condition(this.selectedCondition)?.isUnary) {
55✔
1167
            this.conditionSelect.input.nativeElement.focus();
5✔
1168
        } else {
1169
            const input = this.searchValueInput?.nativeElement || this.picker?.getEditElement();
50✔
1170
            input?.focus();
50✔
1171
        }
1172

1173
        (this.editingInputs?.nativeElement.parentElement as HTMLElement)?.scrollIntoView({block: "nearest", inline: "nearest"});
122✔
1174
    }
1175

1176
    /**
1177
     * @hidden @internal
1178
     */
1179
    public onConditionSelectChanging(event: ISelectionEventArgs) {
1180
        event.cancel = true;
61✔
1181
        this.selectedCondition = event.newSelection.value;
61✔
1182
        this.conditionSelect.close();
61✔
1183
        this.cdr.detectChanges();
61✔
1184
    }
1185

1186
    /**
1187
     * @hidden @internal
1188
     */
1189
    public onKeyDown(eventArgs: KeyboardEvent) {
NEW
1190
        eventArgs.stopPropagation();
×
1191
    }
1192

1193
    /**
1194
     * @hidden @internal
1195
     */
1196
    public onGroupClick(groupContextMenuDropDown: any, targetButton: HTMLButtonElement, groupItem: ExpressionGroupItem) {
1197
        this.exitEditAddMode();
7✔
1198
        this.cdr.detectChanges();
7✔
1199

1200
        this.groupContextMenuDropDown = groupContextMenuDropDown;
7✔
1201
        this.groupContextMenuDropDownOverlaySettings.target = targetButton;
7✔
1202
        this.groupContextMenuDropDownOverlaySettings.positionStrategy = new ConnectedPositioningStrategy({
7✔
1203
            horizontalDirection: HorizontalAlignment.Right,
1204
            horizontalStartPoint: HorizontalAlignment.Left,
1205
            verticalStartPoint: VerticalAlignment.Bottom
1206
        });
1207

1208
        if (groupContextMenuDropDown.collapsed) {
7!
1209
            this.contextualGroup = groupItem;
7✔
1210
            groupContextMenuDropDown.open(this.groupContextMenuDropDownOverlaySettings);
7✔
1211
        } else {
NEW
1212
            groupContextMenuDropDown.close();
×
1213
        }
1214
    }
1215

1216
    /**
1217
     * @hidden @internal
1218
     */
1219
    public getOperator(expressionItem: any) {
1220
        // if (!expressionItem && !this.expressionTree && !this.initialOperator) {
1221
        //     this.initialOperator = 0;
1222
        // }
1223

1224
        const operator = expressionItem ?
32,587✔
1225
            expressionItem.operator :
1226
            this.expressionTree ?
2,521✔
1227
                this.expressionTree.operator :
1228
                this.initialOperator;
1229
        return operator;
32,587✔
1230
    }
1231

1232
    /**
1233
     * @hidden @internal
1234
     */
1235
    public getSwitchGroupText(expressionItem: any) {
1236
        const operator = this.getOperator(expressionItem);
5,420✔
1237
        const condition = operator === FilteringLogic.Or ? this.resourceStrings.igx_query_builder_and_label : this.resourceStrings.igx_query_builder_or_label
5,420✔
1238
        return this.resourceStrings.igx_query_builder_switch_group.replace('{0}', condition.toUpperCase());
5,420✔
1239
    }
1240

1241
    /**
1242
     * @hidden @internal
1243
     */
1244
    public onGroupContextMenuDropDownSelectionChanging(event: ISelectionEventArgs) {
1245
        event.cancel = true;
4✔
1246

1247
        if (event.newSelection.value === 'switchCondition') {
4✔
1248
            const newOperator = (!this.expressionTree ? this.initialOperator : (this.contextualGroup ?? this._expressionTree).operator) === 0 ? 1 : 0;
3!
1249
            this.selectFilteringLogic(newOperator);
3✔
1250
        } else if (event.newSelection.value === 'ungroup') {
1✔
1251
            this.ungroup();
1✔
1252
        }
1253

1254
        this.groupContextMenuDropDown.close();
4✔
1255
    }
1256

1257
    /**
1258
     * @hidden @internal
1259
     */
1260
    public ungroup() {
1261
        const selectedGroup = this.contextualGroup;
1✔
1262
        const parent = selectedGroup.parent;
1✔
1263
        if (parent) {
1✔
1264
            const index = parent.children.indexOf(selectedGroup);
1✔
1265
            parent.children.splice(index, 1, ...selectedGroup.children);
1✔
1266

1267
            for (const expr of selectedGroup.children) {
1✔
1268
                expr.parent = parent;
2✔
1269
            }
1270
        }
1271
        this.commitOperandEdit();
1✔
1272
    }
1273

1274
    /**
1275
     * @hidden @internal
1276
     */
1277
    public selectFilteringLogic(index: number) {
1278
        if (!this.expressionTree) {
3!
NEW
1279
            this.initialOperator = index;
×
NEW
1280
            return;
×
1281
        }
1282

1283
        if (this.contextualGroup) {
3✔
1284
            this.contextualGroup.operator = index as FilteringLogic;
2✔
1285
            this.commitOperandEdit();
2✔
1286
        } else if (this.expressionTree) {
1✔
1287
            this._expressionTree.operator = index as FilteringLogic;
1✔
1288
        }
1289

1290
        this.initialOperator = null;
3✔
1291
    }
1292

1293
    /**
1294
     * @hidden @internal
1295
     */
1296
    public getConditionFriendlyName(name: string): string {
1297
        return this.resourceStrings[`igx_query_builder_filter_${name}`] || name;
36,424!
1298
    }
1299

1300
    /**
1301
     * @hidden @internal
1302
     */
1303
    public isDate(value: any) {
1304
        return value instanceof Date;
6,302✔
1305
    }
1306

1307
    /**
1308
     * @hidden @internal
1309
     */
1310
    public invokeClick(eventArgs: KeyboardEvent) {
1311
        if (!this.dragService.dropGhostChipNode && this.platform.isActivationKey(eventArgs)) {
2✔
1312
            eventArgs.preventDefault();
2✔
1313
            (eventArgs.currentTarget as HTMLElement).click();
2✔
1314
        }
1315
    }
1316

1317
    /**
1318
     * @hidden @internal
1319
     */
1320
    public openPicker(args: KeyboardEvent) {
NEW
1321
        if (this.platform.isActivationKey(args)) {
×
NEW
1322
            args.preventDefault();
×
NEW
1323
            this.picker.open();
×
1324
        }
1325
    }
1326

1327
    /**
1328
     * @hidden @internal
1329
     */
1330
    public onOutletPointerDown(event) {
1331
        // This prevents closing the select's dropdown when clicking the scroll
1332
        event.preventDefault();
53✔
1333
    }
1334

1335
    /**
1336
     * @hidden @internal
1337
     */
1338
    public getConditionList(): string[] {
1339
        if (!this.selectedField) return [];
1,498✔
1340

1341
        if (this.entities?.length === 1 && !this.entities[0].name) {
1,106✔
1342
            return this.selectedField.filters.conditionList();
214✔
1343
        }
1344

1345
        return this.selectedField.filters.extendedConditionList();
892✔
1346
    }
1347

1348
    /**
1349
     * @hidden @internal
1350
     */
1351
    public getFormatter(field: string) {
1352
        return this.fields?.find(el => el.field === field)?.formatter;
26,236✔
1353
    }
1354

1355
    /**
1356
     * @hidden @internal
1357
     */
1358
    public getFormat(field: string) {
1359
        return this.fields?.find(el => el.field === field).pipeArgs.format;
1,749✔
1360
    }
1361

1362
    /**
1363
     * @hidden @internal
1364
     *
1365
     * used by the grid
1366
     */
1367
    public setAddButtonFocus() {
1368
        if (this.addRootAndGroupButton) {
39!
NEW
1369
            this.addRootAndGroupButton.nativeElement.focus();
×
1370
        } else if (this.addConditionButton) {
39✔
1371
            this.addConditionButton.nativeElement.focus();
18✔
1372
        }
1373
    }
1374

1375
    /**
1376
     * @hidden @internal
1377
     */
1378
    public context(expression: ExpressionItem, afterExpression?: ExpressionItem) {
1379
        return {
18,905✔
1380
            $implicit: expression,
1381
            afterExpression
1382
        };
1383
    }
1384

1385
    public formatReturnFields(innerTree: IFilteringExpressionsTree) {
1386
        const returnFields = innerTree.returnFields;
1,228✔
1387
        let text = returnFields.join(', ');
1,228✔
1388
        const innerTreeEntity = this.entities?.find(el => el.name === innerTree.entity);
1,228✔
1389
        if (returnFields.length === innerTreeEntity?.fields.length) {
1,228!
NEW
1390
            text = this.resourceStrings.igx_query_builder_all_fields;
×
1391
        } else {
1392
            text = returnFields.join(', ');
1,228✔
1393
            text = text.length > 25 ? text.substring(0, 25) + ' ...' : text;
1,228!
1394
        }
1395
        return text;
1,228✔
1396
    }
1397

1398
    public isInEditMode(): boolean {
1399
        return !this.parentExpression || (this.parentExpression && this.parentExpression.inEditMode);
24,513✔
1400
    }
1401

1402
    public onInEditModeChanged(expressionItem: ExpressionOperandItem) {
1403
        if (!expressionItem.inEditMode) {
13!
NEW
1404
            this.enterExpressionEdit(expressionItem);
×
1405
        }
1406
    }
1407

1408
    public getExpressionTreeCopy(expressionTree: IExpressionTree, shouldAssignInnerQueryExprTree?: boolean): IExpressionTree {
1409
        if (!expressionTree) {
210✔
1410
            return null;
180✔
1411
        }
1412

1413
        const exprTreeCopy = new FilteringExpressionsTree(expressionTree.operator, expressionTree.fieldName, expressionTree.entity, expressionTree.returnFields);
30✔
1414
        exprTreeCopy.filteringOperands = [];
30✔
1415

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

1418
        if (!this.innerQueryNewExpressionTree && shouldAssignInnerQueryExprTree) {
30✔
1419
            this.innerQueryNewExpressionTree = exprTreeCopy;
18✔
1420
        }
1421

1422
        return exprTreeCopy;
30✔
1423
    }
1424

1425
    public onSelectAllClicked() {
NEW
1426
        if (
×
1427
            (this._selectedReturnFields.length > 0 && this._selectedReturnFields.length < this._selectedEntity.fields.length) ||
×
1428
            this._selectedReturnFields.length == this._selectedEntity.fields.length
1429
        ) {
NEW
1430
            this.returnFieldsCombo.deselectAllItems();
×
1431
        } else {
NEW
1432
            this.returnFieldsCombo.selectAllItems();
×
1433
        }
1434
    }
1435

1436
    public onReturnFieldSelectChanging(event: IComboSelectionChangingEventArgs | ISelectionEventArgs) {
1437
        let newSelection = [];
8✔
1438
        if (Array.isArray(event.newSelection)) {
8✔
1439
            newSelection = event.newSelection.map(item => item.field)
6✔
1440
        } else {
1441
            newSelection.push(event.newSelection.value);
6✔
1442
            this._selectedReturnFields = newSelection;
6✔
1443
        }
1444

1445
        this.initExpressionTree(this.selectedEntity.name, newSelection);
8✔
1446
    }
1447

1448
    public initExpressionTree(selectedEntityName: string, selectedReturnFields: string[]) {
1449
        if (!this._expressionTree) {
45✔
1450
            this._expressionTree = this.createExpressionTreeFromGroupItem(new ExpressionGroupItem(FilteringLogic.And, this.rootGroup), selectedEntityName, selectedReturnFields);
33✔
1451
        }
1452

1453
        if (!this.parentExpression) {
45✔
1454
            this.expressionTreeChange.emit(this._expressionTree);
34✔
1455
        }
1456
    }
1457

1458
    public getSearchValueTemplateContext(defaultSearchValueTemplate): any {
1459
        const ctx = {
1,498✔
1460
            $implicit: this.searchValue,
1461
            selectedField: this.selectedField,
1462
            selectedCondition: this.selectedCondition,
1463
            defaultSearchValueTemplate: defaultSearchValueTemplate
1464
        };
1465
        return ctx;
1,498✔
1466
    }
1467

1468
    private setFormat(field: FieldType) {
1469
        if (!field.pipeArgs) {
2,555✔
1470
            field.pipeArgs = { digitsInfo: DEFAULT_PIPE_DIGITS_INFO };
8✔
1471
        }
1472

1473
        if (!field.pipeArgs.format) {
2,555✔
1474
            field.pipeArgs.format = field.dataType === DataType.Time ?
8!
1475
                DEFAULT_PIPE_TIME_FORMAT : field.dataType === DataType.DateTime ?
8!
1476
                    DEFAULT_PIPE_DATE_TIME_FORMAT : DEFAULT_PIPE_DATE_FORMAT;
1477
        }
1478
    }
1479

1480
    private selectDefaultCondition() {
1481
        if (this.selectedField && this.selectedField.filters) {
142✔
1482
            this.selectedCondition = this.selectedField.filters.conditionList().indexOf('equals') >= 0 ? 'equals' : this.selectedField.filters.conditionList()[0];
106✔
1483
        }
1484
    }
1485

1486
    private setFilters(field: FieldType) {
1487
        if (!field.filters) {
2,555✔
1488
            switch (field.dataType) {
8!
1489
                case DataType.Boolean:
1490
                    field.filters = IgxBooleanFilteringOperand.instance();
2✔
1491
                    break;
2✔
1492
                case DataType.Number:
1493
                case DataType.Currency:
1494
                case DataType.Percent:
1495
                    field.filters = IgxNumberFilteringOperand.instance();
3✔
1496
                    break;
3✔
1497
                case DataType.Date:
1498
                    field.filters = IgxDateFilteringOperand.instance();
1✔
1499
                    break;
1✔
1500
                case DataType.Time:
NEW
1501
                    field.filters = IgxTimeFilteringOperand.instance();
×
NEW
1502
                    break;
×
1503
                case DataType.DateTime:
NEW
1504
                    field.filters = IgxDateTimeFilteringOperand.instance();
×
NEW
1505
                    break;
×
1506
                case DataType.String:
1507
                default:
1508
                    field.filters = IgxStringFilteringOperand.instance();
2✔
1509
                    break;
2✔
1510
            }
1511
        }
1512
    }
1513

1514

1515
    private addGroup(operator: FilteringLogic, parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
1516
        this.cancelOperandAdd();
3✔
1517

1518
        const groupItem = new ExpressionGroupItem(operator, parent);
3✔
1519

1520
        if (parent) {
3!
1521
            if (afterExpression) {
3!
NEW
1522
                const index = parent.children.indexOf(afterExpression);
×
NEW
1523
                parent.children.splice(index + 1, 0, groupItem);
×
1524
            } else {
1525
                parent.children.push(groupItem);
3✔
1526
            }
1527
        } else {
NEW
1528
            this.rootGroup = groupItem;
×
1529
        }
1530

1531
        this.addCondition(groupItem);
3✔
1532
        this.currentGroup = groupItem;
3✔
1533
    }
1534

1535
    private createExpressionGroupItem(expressionTree: IExpressionTree, parent?: ExpressionGroupItem, entityName?: string): ExpressionGroupItem {
1536
        let groupItem: ExpressionGroupItem;
1537
        if (expressionTree) {
663✔
1538
            groupItem = new ExpressionGroupItem(expressionTree.operator, parent);
518✔
1539
            if (!expressionTree.filteringOperands) {
518!
NEW
1540
                return groupItem;
×
1541
            }
1542

1543
            for (let i = 0 ; i < expressionTree.filteringOperands.length; i++) {
518✔
1544
                const expr = expressionTree.filteringOperands[i];
1,143✔
1545

1546
                if (isTree(expr)) {
1,143✔
1547
                    groupItem.children.push(this.createExpressionGroupItem(expr, groupItem, expressionTree.entity));
76✔
1548
                } else {
1549
                    const filteringExpr = expr as IFilteringExpression;
1,067✔
1550
                    const exprCopy: IFilteringExpression = {
1,067✔
1551
                        fieldName: filteringExpr.fieldName,
1552
                        condition: filteringExpr.condition,
1553
                        conditionName: filteringExpr.condition?.name || filteringExpr.conditionName,
1,067!
1554
                        searchVal: filteringExpr.searchVal,
1555
                        searchTree: filteringExpr.searchTree,
1556
                        ignoreCase: filteringExpr.ignoreCase
1557
                    };
1558
                    const operandItem = new ExpressionOperandItem(exprCopy, groupItem);
1,067✔
1559
                    const field = this.fields?.find(el => el.field === filteringExpr.fieldName);
1,731✔
1560
                    operandItem.fieldLabel = field?.label || field?.header || field?.field;
1,067✔
1561
                    if (this._expandedExpressions.filter(e => e.searchTree == operandItem.expression.searchTree).length > 0) {
1,067!
NEW
1562
                        operandItem.expanded = true;
×
1563
                    }
1564
                    groupItem.children.push(operandItem);
1,067✔
1565
                }
1566
            }
1567

1568

1569
            if (expressionTree.entity) {
518✔
1570
                entityName = expressionTree.entity;
461✔
1571
            }
1572
            const entity = this.entities?.find(el => el.name === entityName);
781✔
1573
            if (entity) {
518✔
1574
                this.fields = entity.fields;
461✔
1575
            }
1576

1577
            this._selectedEntity = this.entities?.find(el => el.name === entityName);
781✔
1578
            this._selectedReturnFields =
518✔
1579
                !expressionTree.returnFields || expressionTree.returnFields.includes('*') || expressionTree.returnFields.includes('All') || expressionTree.returnFields.length === 0
2,112✔
1580
                    ? this.fields?.map(f => f.field)
1,101✔
1581
                    : this.fields?.filter(f => expressionTree.returnFields.indexOf(f.field) >= 0).map(f => f.field);
1,137✔
1582
        }
1583
        return groupItem;
663✔
1584
    }
1585

1586
    private createExpressionTreeFromGroupItem(groupItem: ExpressionGroupItem, entity?: string, returnFields?: string[]): FilteringExpressionsTree {
1587
        if (!groupItem) {
147✔
1588
            return null;
4✔
1589
        }
1590

1591
        const expressionTree = new FilteringExpressionsTree(groupItem.operator, undefined, entity, returnFields);
143✔
1592

1593
        for (let i = 0; i < groupItem.children.length; i++) {
143✔
1594
            const item = groupItem.children[i];
194✔
1595

1596
            if (item instanceof ExpressionGroupItem) {
194✔
1597
                const subTree = this.createExpressionTreeFromGroupItem((item as ExpressionGroupItem), entity, returnFields);
18✔
1598
                expressionTree.filteringOperands.push(subTree);
18✔
1599
            } else {
1600
                expressionTree.filteringOperands.push((item as ExpressionOperandItem).expression);
176✔
1601
            }
1602
        }
1603

1604
        return expressionTree;
143✔
1605
    }
1606

1607
    private scrollElementIntoView(target: HTMLElement) {
1608
        const container = this.expressionsContainer.nativeElement;
478✔
1609
        const targetOffset = target.offsetTop - container.offsetTop;
478✔
1610
        const delta = 10;
478✔
1611

1612
        if (container.scrollTop + delta > targetOffset) {
478✔
1613
            container.scrollTop = targetOffset - delta;
268✔
1614
        } else if (container.scrollTop + container.clientHeight < targetOffset + target.offsetHeight + delta) {
210✔
1615
            container.scrollTop = targetOffset + target.offsetHeight + delta - container.clientHeight;
205✔
1616
        }
1617
    }
1618

1619
    private focusEditedExpressionChip() {
1620
        if (this._timeoutId) {
58✔
1621
            clearTimeout(this._timeoutId);
5✔
1622
        }
1623

1624
        this._timeoutId = setTimeout(() => {
58✔
1625
            if (this._lastFocusedChipIndex != -1) {
58✔
1626
                //Sort the expression chip list. 
1627
                //If there was a recent drag&drop and the tree hasn't rerendered(child query), they will be unordered
1628
                const sortedChips = this.expressionsChips.toArray().sort(function (a, b) {
56✔
1629
                    if (a === b) return 0;
70!
1630
                    if (a.chipArea.nativeElement.compareDocumentPosition(b.chipArea.nativeElement) & 2) {
70✔
1631
                        // b comes before a
1632
                        return 1;
57✔
1633
                    }
1634
                    return -1;
13✔
1635
                });
1636
                const chipElement = sortedChips[this._lastFocusedChipIndex]?.nativeElement;
56✔
1637
                if (chipElement) {
56✔
1638
                    chipElement.focus();
48✔
1639
                }
1640
                this._lastFocusedChipIndex = -1;
56✔
1641
                this._focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
56✔
1642
            }
1643
        }, this._focusDelay);
1644
    }
1645

1646
    private init() {
1647
        this.cancelOperandAdd();
400✔
1648
        this.cancelOperandEdit();
400✔
1649

1650
        // Ignore values of certain properties for the comparison
1651
        const propsToIgnore = ['parent', 'hovered', 'ignoreCase', 'inEditMode', 'inAddMode'];
400✔
1652
        const propsReplacer = function replacer(key, value) {
400✔
1653
            if (propsToIgnore.indexOf(key) >= 0) {
16,299✔
1654
                return undefined;
2,162✔
1655
            } else {
1656
                return value;
14,137✔
1657
            }
1658
        };
1659

1660
        // Skip root being recreated if the same
1661
        const newRootGroup = this.createExpressionGroupItem(this.expressionTree);
400✔
1662
        if (JSON.stringify(this.rootGroup, propsReplacer) !== JSON.stringify(newRootGroup, propsReplacer)) {
400✔
1663
            this.rootGroup = this.createExpressionGroupItem(this.expressionTree);
182✔
1664
            this.currentGroup = this.rootGroup;
182✔
1665
        }
1666

1667
        if (this.rootGroup?.children?.length == 0) {
400✔
1668
            this.rootGroup = null;
40✔
1669
            this.currentGroup = null;
40✔
1670
        }
1671
    }
1672
}
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