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

IgniteUI / igniteui-angular / 13390451169

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

Pull #14647

github

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

13327 of 15595 branches covered (85.46%)

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

1 existing line in 1 file now uncovered.

26882 of 29319 relevant lines covered (91.69%)

33815.97 hits per line

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

91.68
/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';
1✔
62
const DEFAULT_PIPE_TIME_FORMAT = 'mediumTime';
1✔
63
const DEFAULT_PIPE_DATE_TIME_FORMAT = 'medium';
1✔
64
const DEFAULT_PIPE_DIGITS_INFO = '1.0-3';
1✔
65
const DEFAULT_CHIP_FOCUS_DELAY = 50;
1✔
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 {
1✔
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;
66,134✔
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,196✔
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) => {
207✔
844
        //console.log('deleteItem', expressionItem)
845
        if (!expressionItem.parent) {
47✔
846
            this.rootGroup = null;
15✔
847
            this.currentGroup = null;
15✔
848
            //this._expressionTree = null;
849
            return;
15✔
850
        }
851

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

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

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

867
        if (!this.parentExpression) {
32✔
868
            this.expressionTreeChange.emit(this._expressionTree);
31✔
869
        }
870
    }
871

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1032
    public dragService: IgxQueryBuilderDragService = new IgxQueryBuilderDragService(this, this.el, this.deleteItem, this.focusChipAfterDrag);
207✔
1033

1034
    //Chip can be dragged if its tree is in edit mode and there is no inner query that's been edited
1035
    public canBeDragged(): boolean {
1036
        return this.isInEditMode && (!this.innerQueries || this.innerQueries.length == 0 || !this.innerQueries?.some(q => q.isInEditMode()))
8,995✔
1037
    }
1038

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

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

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

1068

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

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

1090
        this.addExpressionItemDropDown.open(this.addExpressionDropDownOverlaySettings);
4✔
1091
    }
1092

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

1101
        if (this.parentExpression) {
1!
NEW
1102
            this.inEditModeChange.emit(this.parentExpression);
×
1103
        }
1104

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

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

1125
        if (this.parentExpression) {
122✔
1126
            this.inEditModeChange.emit(this.parentExpression);
13✔
1127
        }
1128

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

1143
        expressionItem.inEditMode = true;
122✔
1144
        this._editedExpression = expressionItem;
122✔
1145
        this.cdr.detectChanges();
122✔
1146

1147
        this.entitySelectOverlaySettings.target = this.entitySelect.element;
122✔
1148
        this.entitySelectOverlaySettings.excludeFromOutsideClick = [this.entitySelect.element as HTMLElement];
122✔
1149
        this.entitySelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
122✔
1150

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

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

1176
        (this.editingInputs?.nativeElement.parentElement as HTMLElement)?.scrollIntoView({block: "nearest", inline: "nearest"});
122✔
1177
    }
1178

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

1189
    /**
1190
     * @hidden @internal
1191
     */
1192
    public onKeyDown(eventArgs: KeyboardEvent) {
NEW
1193
        eventArgs.stopPropagation();
×
1194
    }
1195

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

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

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

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

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

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

1244
    /**
1245
     * @hidden @internal
1246
     */
1247
    public onGroupContextMenuDropDownSelectionChanging(event: ISelectionEventArgs) {
1248
        event.cancel = true;
4✔
1249

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

1257
        this.groupContextMenuDropDown.close();
4✔
1258
    }
1259

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

1270
            for (const expr of selectedGroup.children) {
1✔
1271
                expr.parent = parent;
2✔
1272
            }
1273
        }
1274
        this.commitOperandEdit();
1✔
1275
    }
1276

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

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

1293
        this.initialOperator = null;
3✔
1294
    }
1295

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

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

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

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

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

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

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

1348
        return this.selectedField.filters.extendedConditionList();
892✔
1349
    }
1350

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

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

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

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

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

1401
    public isInEditMode(): boolean {
1402
        return !this.parentExpression || (this.parentExpression && this.parentExpression.inEditMode);
29,071✔
1403
    }
1404

1405
    public onInEditModeChanged(expressionItem: ExpressionOperandItem) {
1406
        if (!expressionItem.inEditMode) {
13!
NEW
1407
            this.enterExpressionEdit(expressionItem);
×
1408
        }
1409
    }
1410

1411
    public getExpressionTreeCopy(expressionTree: IExpressionTree, shouldAssignInnerQueryExprTree?: boolean): IExpressionTree {
1412
        if (!expressionTree) {
210✔
1413
            return null;
180✔
1414
        }
1415

1416
        const exprTreeCopy = new FilteringExpressionsTree(expressionTree.operator, expressionTree.fieldName, expressionTree.entity, expressionTree.returnFields);
30✔
1417
        exprTreeCopy.filteringOperands = [];
30✔
1418

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

1421
        if (!this.innerQueryNewExpressionTree && shouldAssignInnerQueryExprTree) {
30✔
1422
            this.innerQueryNewExpressionTree = exprTreeCopy;
18✔
1423
        }
1424

1425
        return exprTreeCopy;
30✔
1426
    }
1427

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

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

1448
        this.initExpressionTree(this.selectedEntity.name, newSelection);
8✔
1449
    }
1450

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

1456
        if (!this.parentExpression) {
45✔
1457
            this.expressionTreeChange.emit(this._expressionTree);
34✔
1458
        }
1459
    }
1460

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

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

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

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

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

1517

1518
    private addGroup(operator: FilteringLogic, parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
1519
        this.cancelOperandAdd();
3✔
1520

1521
        const groupItem = new ExpressionGroupItem(operator, parent);
3✔
1522

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

1534
        this.addCondition(groupItem);
3✔
1535
        this.currentGroup = groupItem;
3✔
1536
    }
1537

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

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

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

1571

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

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

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

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

1596
        for (let i = 0; i < groupItem.children.length; i++) {
143✔
1597
            const item = groupItem.children[i];
194✔
1598

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

1607
        return expressionTree;
143✔
1608
    }
1609

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

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

1622
    private focusEditedExpressionChip() {
1623
        if (this._timeoutId) {
58✔
1624
            clearTimeout(this._timeoutId);
5✔
1625
        }
1626

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

1649
    private init() {
1650
        this.cancelOperandAdd();
400✔
1651
        this.cancelOperandEdit();
400✔
1652

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

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

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