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

IgniteUI / igniteui-angular / 13497671636

24 Feb 2025 12:09PM UTC coverage: 91.651% (+0.03%) from 91.619%
13497671636

Pull #14647

github

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

13324 of 15595 branches covered (85.44%)

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

1 existing line in 1 file now uncovered.

26872 of 29320 relevant lines covered (91.65%)

33805.47 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
    public dragService: IgxQueryBuilderDragService = new IgxQueryBuilderDragService(this, this.el, this.deleteItem, this.focusChipAfterDrag);
207✔
1032

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

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

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

1062

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

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

1084
        this.addExpressionItemDropDown.open(this.addExpressionDropDownOverlaySettings);
4✔
1085
    }
1086

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

1095
        if (this.parentExpression) {
1!
NEW
1096
            this.inEditModeChange.emit(this.parentExpression);
×
1097
        }
1098

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

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

1119
        if (this.parentExpression) {
122✔
1120
            this.inEditModeChange.emit(this.parentExpression);
13✔
1121
        }
1122

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

1137
        expressionItem.inEditMode = true;
122✔
1138
        this._editedExpression = expressionItem;
122✔
1139
        this.cdr.detectChanges();
122✔
1140

1141
        this.entitySelectOverlaySettings.target = this.entitySelect.element;
122✔
1142
        this.entitySelectOverlaySettings.excludeFromOutsideClick = [this.entitySelect.element as HTMLElement];
122✔
1143
        this.entitySelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
122✔
1144

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

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

1170
        (this.editingInputs?.nativeElement.parentElement as HTMLElement)?.scrollIntoView({block: "nearest", inline: "nearest"});
122✔
1171
    }
1172

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

1183
    /**
1184
     * @hidden @internal
1185
     */
1186
    public onKeyDown(eventArgs: KeyboardEvent) {
NEW
1187
        eventArgs.stopPropagation();
×
1188
    }
1189

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

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

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

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

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

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

1238
    /**
1239
     * @hidden @internal
1240
     */
1241
    public onGroupContextMenuDropDownSelectionChanging(event: ISelectionEventArgs) {
1242
        event.cancel = true;
4✔
1243

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

1251
        this.groupContextMenuDropDown.close();
4✔
1252
    }
1253

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

1264
            for (const expr of selectedGroup.children) {
1✔
1265
                expr.parent = parent;
2✔
1266
            }
1267
        }
1268
        this.commitOperandEdit();
1✔
1269
    }
1270

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

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

1287
        this.initialOperator = null;
3✔
1288
    }
1289

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

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

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

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

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

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

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

1342
        return this.selectedField.filters.extendedConditionList();
892✔
1343
    }
1344

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

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

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

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

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

1395
    public isInEditMode(): boolean {
1396
        return !this.parentExpression || (this.parentExpression && this.parentExpression.inEditMode);
24,513✔
1397
    }
1398

1399
    public onInEditModeChanged(expressionItem: ExpressionOperandItem) {
1400
        if (!expressionItem.inEditMode) {
13!
NEW
1401
            this.enterExpressionEdit(expressionItem);
×
1402
        }
1403
    }
1404

1405
    public getExpressionTreeCopy(expressionTree: IExpressionTree, shouldAssignInnerQueryExprTree?: boolean): IExpressionTree {
1406
        if (!expressionTree) {
210✔
1407
            return null;
180✔
1408
        }
1409

1410
        const exprTreeCopy = new FilteringExpressionsTree(expressionTree.operator, expressionTree.fieldName, expressionTree.entity, expressionTree.returnFields);
30✔
1411
        exprTreeCopy.filteringOperands = [];
30✔
1412

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

1415
        if (!this.innerQueryNewExpressionTree && shouldAssignInnerQueryExprTree) {
30✔
1416
            this.innerQueryNewExpressionTree = exprTreeCopy;
18✔
1417
        }
1418

1419
        return exprTreeCopy;
30✔
1420
    }
1421

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

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

1442
        this.initExpressionTree(this.selectedEntity.name, newSelection);
8✔
1443
    }
1444

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

1450
        if (!this.parentExpression) {
45✔
1451
            this.expressionTreeChange.emit(this._expressionTree);
34✔
1452
        }
1453
    }
1454

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

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

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

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

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

1511

1512
    private addGroup(operator: FilteringLogic, parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
1513
        this.cancelOperandAdd();
3✔
1514

1515
        const groupItem = new ExpressionGroupItem(operator, parent);
3✔
1516

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

1528
        this.addCondition(groupItem);
3✔
1529
        this.currentGroup = groupItem;
3✔
1530
    }
1531

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

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

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

1565

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

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

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

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

1590
        for (let i = 0; i < groupItem.children.length; i++) {
143✔
1591
            const item = groupItem.children[i];
194✔
1592

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

1601
        return expressionTree;
143✔
1602
    }
1603

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

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

1616
    private focusEditedExpressionChip() {
1617
        if (this._timeoutId) {
58✔
1618
            clearTimeout(this._timeoutId);
5✔
1619
        }
1620

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

1643
    private init() {
1644
        this.cancelOperandAdd();
400✔
1645
        this.cancelOperandEdit();
400✔
1646

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

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

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