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

IgniteUI / igniteui-angular / 13525400762

25 Feb 2025 03:54PM UTC coverage: 91.641% (+0.02%) from 91.618%
13525400762

push

github

web-flow
feat(query-builder): support for nested queries and other improvements (#14647)

Co-authored-by: ivanvpetrov <110455887+ivanvpetrov@users.noreply.github.com>
Co-authored-by: Teodosia Hristodorova <52423497+teodosiah@users.noreply.github.com>
Co-authored-by: Mike Cherkasov <otixa.scythe@gmail.com>
Co-authored-by: Galina Edinakova <gedinakova@infragistics.com>
Co-authored-by: Simeon Simeonoff <sim.simeonoff@gmail.com>
Co-authored-by: desig9stein <mspopovv@gmail.com>
Co-authored-by: Rumyana Andriova <54146583+randriova@users.noreply.github.com>
Co-authored-by: Damyan Petev <damyanpetev@users.noreply.github.com>

13327 of 15595 branches covered (85.46%)

897 of 982 new or added lines in 20 files covered. (91.34%)

2 existing lines in 2 files now uncovered.

26871 of 29322 relevant lines covered (91.64%)

33811.54 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 { IgxPickerToggleComponent, IgxPickerClearComponent } from '../date-common/picker-icons.common';
35
import { IgxInputDirective } from '../directives/input/input.directive';
36
import { IgxInputGroupComponent } from '../input-group/input-group.component';
37
import { IgxSelectItemComponent } from '../select/select-item.component';
38
import { IgxPrefixDirective } from '../directives/prefix/prefix.directive';
39
import { IgxIconComponent } from '../icon/icon.component';
40
import { getCurrentResourceStrings } from '../core/i18n/resources';
41
import { IgxIconButtonDirective } from '../directives/button/icon-button.directive';
42
import { IComboSelectionChangingEventArgs, IgxComboComponent } from "../combo/combo.component";
43
import { IgxComboHeaderDirective } from '../combo/public_api';
44
import { IgxCheckboxComponent } from "../checkbox/checkbox.component";
45
import { IChangeCheckboxEventArgs } from '../checkbox/checkbox-base.directive';
46
import { IgxDialogComponent } from "../dialog/dialog.component";
47
import { ISelectionEventArgs } from '../drop-down/drop-down.common';
48
import { IgxTooltipDirective } from '../directives/tooltip/tooltip.directive';
49
import { IgxTooltipTargetDirective } from '../directives/tooltip/tooltip-target.directive';
50
import { IgxQueryBuilderSearchValueTemplateDirective } from './query-builder.directives';
51
import { IgxQueryBuilderComponent } from './query-builder.component';
52
import { IgxDragIgnoreDirective, IgxDropDirective } from '../directives/drag-drop/drag-drop.directive';
53
import { IgxDropDownComponent } from '../drop-down/drop-down.component';
54
import { IgxDropDownItemComponent } from '../drop-down/drop-down-item.component';
55
import { IgxDropDownItemNavigationDirective } from '../drop-down/drop-down-navigation.directive';
56
import { IgxQueryBuilderDragService } from './query-builder-drag.service';
57
import { isTree } from '../data-operations/expressions-tree-util';
58
import { ExpressionGroupItem, ExpressionItem, ExpressionOperandItem, IgxFieldFormatterPipe } from './query-builder.common';
59

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

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

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

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

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

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

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

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

154
        return this._fields;
22,861✔
155
    }
156

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

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

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

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

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

194
        if (!this._preventInit) {
412✔
195
            this.init();
403✔
196
        }
197
    }
198

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

292
    @ViewChild('editingInputsContainer', { read: ElementRef })
293
    protected set editingInputsContainer(value: ElementRef) {
294
        if ((value && !this._editingInputsContainer) ||
592✔
295
            (value && this._editingInputsContainer && this._editingInputsContainer.nativeElement !== value.nativeElement)) {
296
            requestAnimationFrame(() => {
202✔
297
                this.scrollElementIntoView(value.nativeElement);
202✔
298
            });
299
        }
300

301
        this._editingInputsContainer = value;
592✔
302
    }
303

304
    /** @hidden */
305
    protected get editingInputsContainer(): ElementRef {
NEW
306
        return this._editingInputsContainer;
×
307
    }
308

309
    @ViewChild('currentGroupButtonsContainer', { read: ElementRef })
310
    protected set currentGroupButtonsContainer(value: ElementRef) {
311
        if ((value && !this._currentGroupButtonsContainer) ||
592✔
312
            (value && this._currentGroupButtonsContainer && this._currentGroupButtonsContainer.nativeElement !== value.nativeElement)) {
313
            requestAnimationFrame(() => {
280✔
314
                this.scrollElementIntoView(value.nativeElement);
280✔
315
            });
316
        }
317

318
        this._currentGroupButtonsContainer = value;
592✔
319
    }
320

321
    /** @hidden */
322
    protected get currentGroupButtonsContainer(): ElementRef {
NEW
323
        return this._currentGroupButtonsContainer;
×
324
    }
325

326
    @ViewChild('expressionsContainer')
327
    private expressionsContainer: ElementRef;
328

329
    @ViewChild('overlayOutlet', { read: IgxOverlayOutletDirective, static: true })
330
    private overlayOutlet: IgxOverlayOutletDirective;
331

332
    @ViewChildren(IgxQueryBuilderTreeComponent)
333
    private innerQueries: QueryList<IgxQueryBuilderTreeComponent>;
334

335
    /**
336
     * @hidden @internal
337
     */
338
    public innerQueryNewExpressionTree: IExpressionTree;
339

340
    /**
341
     * @hidden @internal
342
     */
343
    public rootGroup: ExpressionGroupItem;
344

345
    /**
346
     * @hidden @internal
347
     */
348
    public selectedExpressions: ExpressionOperandItem[] = [];
209✔
349

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

355
    /**
356
     * @hidden @internal
357
     */
358
    public contextualGroup: ExpressionGroupItem;
359

360
    /**
361
     * @hidden @internal
362
     */
363
    public filteringLogics;
364

365
    /**
366
     * @hidden @internal
367
     */
368
    public selectedCondition: string;
369

370
    /**
371
     * @hidden @internal
372
     */
373
    public searchValue: { value: any } = { value: null };
209✔
374

375
    /**
376
     * @hidden @internal
377
     */
378
    public pickerOutlet: IgxOverlayOutletDirective | ElementRef;
379

380
    /**
381
     * @hidden @internal
382
     */
383
    public prevFocusedExpression: ExpressionOperandItem;
384

385
    /**
386
     * @hidden @internal
387
     */
388
    public initialOperator = 0;
209✔
389

390
    /**
391
     * @hidden @internal
392
     */
393
    public returnFieldSelectOverlaySettings: OverlaySettings = {
209✔
394
        scrollStrategy: new AbsoluteScrollStrategy(),
395
        modal: false,
396
        closeOnOutsideClick: true
397
    };
398

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

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

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

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

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

444
    private destroy$ = new Subject<any>();
209✔
445
    private _timeoutId: any;
446
    private _lastFocusedChipIndex: number;
447
    private _focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
209✔
448
    private _parentExpression: ExpressionOperandItem;
449
    private _selectedEntity: EntityType;
450
    private _selectedReturnFields: string | string[];
451
    private _selectedField: FieldType;
452
    private _editingInputsContainer: ElementRef;
453
    private _currentGroupButtonsContainer: ElementRef;
454
    private _addModeExpression: ExpressionOperandItem;
455
    private _editedExpression: ExpressionOperandItem;
456
    private _preventInit = false;
209✔
457
    private _prevFocusedContainer: ElementRef;
458
    private _expandedExpressions: IFilteringExpression[] = [];
209✔
459
    private _fields: FieldType[];
460
    private _expressionTree: IExpressionTree;
461
    private _locale;
462
    private _entityNewValue: EntityType;
463
    private _resourceStrings = getCurrentResourceStrings(QueryBuilderResourceStringsEN);
209✔
464

465
    /**
466
     * Returns if the select entity dropdown at the root level is disabled after the initial selection.
467
     */
468
    public get disableEntityChange(): boolean {
469

470
        return !this.parentExpression && this.selectedEntity ? this.queryBuilder.disableEntityChange : false;
4,954✔
471
    }
472

473
    /**
474
     * Returns if the fields combo at the root level is disabled.
475
     */
476
     public get disableReturnFieldsChange(): boolean {
477

478
        return !this.selectedEntity || this.queryBuilder.disableReturnFieldsChange;
3,246✔
479
    }
480

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

746
        this._focusDelay = 250;
67✔
747

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

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

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

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

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

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

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

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

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

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

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

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

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

844
    private deleteItem = (expressionItem: ExpressionItem, skipEmit: boolean = false) => {
209✔
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, true);
16✔
865
        }
866

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

872
    /**
873
     * @hidden @internal
874
     */
875
    public cancelOperandEdit() {
876
        if (this.innerQueries) {
430✔
877
            const innerQuery = this.innerQueries.filter(q => q.isInEditMode())[0];
221✔
878
            if (innerQuery) {
221✔
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) {
430✔
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) {
430✔
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,388✔
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) => {
209✔
1028
        this._lastFocusedChipIndex = index;
10✔
1029
        this.focusEditedExpressionChip();
10✔
1030
    }
1031

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

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

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

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

1066

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1294
    /**
1295
     * @hidden @internal
1296
     */
1297
    public getConditionFriendlyName(name: string): string {
1298
        // As we have an 'In' condition already used in ESF to search in a Set, we add the 'Query' suffix to the newly introduced nested query condition names.
1299
        // So instead of in/notIn we end up with 'inQuery'/'notInQuery', hence removing the suffix from the friendly name.
1300
        return this.resourceStrings[`igx_query_builder_filter_${name?.replace('Query', '')}`] || name;
36,440✔
1301
    }
1302

1303
    /**
1304
     * @hidden @internal
1305
     */
1306
    public isDate(value: any) {
1307
        return value instanceof Date;
6,312✔
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,270✔
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,931✔
1383
            $implicit: expression,
1384
            afterExpression
1385
        };
1386
    }
1387

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

1401
    public isInEditMode(): boolean {
1402
        return !this.parentExpression || (this.parentExpression && this.parentExpression.inEditMode);
24,551✔
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,579✔
1473
            field.pipeArgs = { digitsInfo: DEFAULT_PIPE_DIGITS_INFO };
8✔
1474
        }
1475

1476
        if (!field.pipeArgs.format) {
2,579✔
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,579✔
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) {
670✔
1541
            groupItem = new ExpressionGroupItem(expressionTree.operator, parent);
524✔
1542
            if (!expressionTree.filteringOperands) {
524!
NEW
1543
                return groupItem;
×
1544
            }
1545

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

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

1571

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

1580
            this._selectedEntity = this.entities?.find(el => el.name === entityName);
791✔
1581
            this._selectedReturnFields =
524✔
1582
                !expressionTree.returnFields || expressionTree.returnFields.includes('*') || expressionTree.returnFields.includes('All') || expressionTree.returnFields.length === 0
2,134✔
1583
                    ? this.fields?.map(f => f.field)
1,117✔
1584
                    : this.fields?.filter(f => expressionTree.returnFields.indexOf(f.field) >= 0).map(f => f.field);
1,145✔
1585
        }
1586
        return groupItem;
670✔
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;
482✔
1612
        const targetOffset = target.offsetTop - container.offsetTop;
482✔
1613
        const delta = 10;
482✔
1614

1615
        if (container.scrollTop + delta > targetOffset) {
482✔
1616
            container.scrollTop = targetOffset - delta;
272✔
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();
403✔
1651
        this.cancelOperandEdit();
403✔
1652

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

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

1670
        if (this.rootGroup?.children?.length == 0) {
403✔
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