57 changed files with 3015 additions and 43 deletions
@ -0,0 +1,30 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div fxLayout="row" [formGroup]="booleanFilterPredicateFormGroup"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>filter.operation.operation</mat-label> |
|||
<mat-select required formControlName="operation"> |
|||
<mat-option *ngFor="let operation of booleanOperations" [value]="operation"> |
|||
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-checkbox formControlName="value"> |
|||
{{ 'filter.value' | translate }} |
|||
</mat-checkbox> |
|||
</div> |
|||
@ -0,0 +1,101 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import { |
|||
BooleanFilterPredicate, |
|||
BooleanOperation, booleanOperationTranslationMap, |
|||
FilterPredicateType |
|||
} from '@shared/models/query/query.models'; |
|||
import { isDefined } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-boolean-filter-predicate', |
|||
templateUrl: './boolean-filter-predicate.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => BooleanFilterPredicateComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class BooleanFilterPredicateComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() userMode: boolean; |
|||
|
|||
booleanFilterPredicateFormGroup: FormGroup; |
|||
|
|||
booleanOperations = Object.keys(BooleanOperation); |
|||
booleanOperationEnum = BooleanOperation; |
|||
booleanOperationTranslations = booleanOperationTranslationMap; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.booleanFilterPredicateFormGroup = this.fb.group({ |
|||
operation: [BooleanOperation.EQUAL, [Validators.required]], |
|||
value: [false] |
|||
}); |
|||
if (this.userMode) { |
|||
this.booleanFilterPredicateFormGroup.get('operation').disable({emitEvent: false}); |
|||
} |
|||
this.booleanFilterPredicateFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.booleanFilterPredicateFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.booleanFilterPredicateFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(predicate: BooleanFilterPredicate): void { |
|||
this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); |
|||
this.booleanFilterPredicateFormGroup.get('value').patchValue(isDefined(predicate.value) ? predicate.value : false, {emitEvent: false}); |
|||
} |
|||
|
|||
private updateModel() { |
|||
let predicate: BooleanFilterPredicate = null; |
|||
if (this.booleanFilterPredicateFormGroup.valid) { |
|||
predicate = this.booleanFilterPredicateFormGroup.getRawValue(); |
|||
if (!isDefined(predicate.value)) { |
|||
predicate.value = false; |
|||
} |
|||
predicate.type = FilterPredicateType.BOOLEAN; |
|||
} |
|||
this.propagateChange(predicate); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()"> |
|||
<mat-toolbar color="primary"> |
|||
<h2 translate>filter.complex-filter</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async" fxLayout="column"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>filter.operation.operation</mat-label> |
|||
<mat-select required formControlName="operation"> |
|||
<mat-option *ngFor="let operation of complexOperations" [value]="operation"> |
|||
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<tb-filter-predicate-list |
|||
[userMode]="data.userMode" |
|||
[valueType]="data.valueType" |
|||
formControlName="predicates"> |
|||
</tb-filter-predicate-list> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid"> |
|||
{{ 'action.update' | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" |
|||
cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,94 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; |
|||
import { ErrorStateMatcher } from '@angular/material/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { |
|||
BooleanOperation, booleanOperationTranslationMap, |
|||
ComplexFilterPredicate, ComplexOperation, complexOperationTranslationMap, |
|||
EntityKeyValueType, |
|||
FilterPredicateType |
|||
} from '@shared/models/query/query.models'; |
|||
|
|||
export interface ComplexFilterPredicateDialogData { |
|||
complexPredicate: ComplexFilterPredicate; |
|||
userMode: boolean; |
|||
disabled: boolean; |
|||
valueType: EntityKeyValueType; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-complex-filter-predicate-dialog', |
|||
templateUrl: './complex-filter-predicate-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: ComplexFilterPredicateDialogComponent}], |
|||
styleUrls: [] |
|||
}) |
|||
export class ComplexFilterPredicateDialogComponent extends |
|||
DialogComponent<ComplexFilterPredicateDialogComponent, ComplexFilterPredicate> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
complexFilterFormGroup: FormGroup; |
|||
|
|||
complexOperations = Object.keys(ComplexOperation); |
|||
complexOperationEnum = ComplexOperation; |
|||
complexOperationTranslations = complexOperationTranslationMap; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: ComplexFilterPredicateDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<ComplexFilterPredicateDialogComponent, ComplexFilterPredicate>, |
|||
private fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.complexFilterFormGroup = this.fb.group( |
|||
{ |
|||
operation: [this.data.complexPredicate.operation, [Validators.required]], |
|||
predicates: [this.data.complexPredicate.predicates, [Validators.required]] |
|||
} |
|||
); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = !!(control && control.invalid && this.submitted); |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
if (this.complexFilterFormGroup.valid) { |
|||
const predicate: ComplexFilterPredicate = this.complexFilterFormGroup.getRawValue(); |
|||
predicate.type = FilterPredicateType.COMPLEX; |
|||
this.dialogRef.close(predicate); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div fxLayout="row"> |
|||
<mat-label translate>filter.complex-filter</mat-label> |
|||
<button mat-icon-button color="primary" |
|||
[fxShow]="!disabled" |
|||
type="button" |
|||
(click)="openComplexFilterDialog()" |
|||
matTooltip="{{ 'filter.edit-complex-filter' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>edit</mat-icon> |
|||
</button> |
|||
</div> |
|||
@ -0,0 +1,97 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
|||
import { ComplexFilterPredicate, EntityKeyValueType } from '@shared/models/query/query.models'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { |
|||
ComplexFilterPredicateDialogComponent, |
|||
ComplexFilterPredicateDialogData |
|||
} from '@home/components/filter/complex-filter-predicate-dialog.component'; |
|||
import { deepClone } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-complex-filter-predicate', |
|||
templateUrl: './complex-filter-predicate.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ComplexFilterPredicateComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class ComplexFilterPredicateComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() userMode: boolean; |
|||
|
|||
@Input() valueType: EntityKeyValueType; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
private complexFilterPredicate: ComplexFilterPredicate; |
|||
|
|||
constructor(private dialog: MatDialog) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
writeValue(predicate: ComplexFilterPredicate): void { |
|||
this.complexFilterPredicate = predicate; |
|||
} |
|||
|
|||
private openComplexFilterDialog() { |
|||
this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData, |
|||
ComplexFilterPredicate>(ComplexFilterPredicateDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
complexPredicate: deepClone(this.complexFilterPredicate), |
|||
disabled: this.disabled, |
|||
userMode: this.userMode, |
|||
valueType: this.valueType |
|||
} |
|||
}).afterClosed().subscribe( |
|||
(result) => { |
|||
if (result) { |
|||
this.complexFilterPredicate = result; |
|||
this.updateModel(); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.propagateChange(this.complexFilterPredicate); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="min-width: 480px;"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<div fxFlex fxLayout="column"> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>filter.name</mat-label> |
|||
<input matInput formControlName="filter" required> |
|||
<mat-error *ngIf="filterFormGroup.get('filter').hasError('required')"> |
|||
{{ 'filter.name-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="filterFormGroup.get('filter').hasError('duplicateFilterName')"> |
|||
{{ 'filter.duplicate-filter' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<tb-key-filter-list |
|||
formControlName="keyFilters" |
|||
[userMode]="userMode"> |
|||
</tb-key-filter-list> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty"> |
|||
{{ (isAdd ? 'action.add' : 'action.update') | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,137 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; |
|||
import { ErrorStateMatcher } from '@angular/material/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
FormGroupDirective, |
|||
NgForm, |
|||
ValidatorFn, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { Filter, Filters } from '@shared/models/query/query.models'; |
|||
|
|||
export interface FilterDialogData { |
|||
isAdd: boolean; |
|||
userMode: boolean; |
|||
filters: Filters | Array<Filter>; |
|||
filter?: Filter; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-filter-dialog', |
|||
templateUrl: './filter-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: FilterDialogComponent}], |
|||
styleUrls: [] |
|||
}) |
|||
export class FilterDialogComponent extends DialogComponent<FilterDialogComponent, Filter> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
isAdd: boolean; |
|||
userMode: boolean; |
|||
filters: Array<Filter>; |
|||
|
|||
filter: Filter; |
|||
|
|||
filterFormGroup: FormGroup; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: FilterDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<FilterDialogComponent, Filter>, |
|||
private fb: FormBuilder, |
|||
private utils: UtilsService, |
|||
public translate: TranslateService) { |
|||
super(store, router, dialogRef); |
|||
this.isAdd = data.isAdd; |
|||
this.userMode = data.userMode; |
|||
if (Array.isArray(data.filters)) { |
|||
this.filters = data.filters; |
|||
} else { |
|||
this.filters = []; |
|||
for (const filterId of Object.keys(data.filters)) { |
|||
this.filters.push(data.filters[filterId]); |
|||
} |
|||
} |
|||
if (this.isAdd && !this.data.filter) { |
|||
this.filter = { |
|||
id: null, |
|||
filter: '', |
|||
keyFilters: [] |
|||
}; |
|||
} else { |
|||
this.filter = data.filter; |
|||
} |
|||
|
|||
this.filterFormGroup = this.fb.group({ |
|||
filter: [this.filter.filter, [this.validateDuplicateFilterName(), Validators.required]], |
|||
keyFilters: [this.filter.keyFilters, Validators.required] |
|||
}); |
|||
} |
|||
|
|||
validateDuplicateFilterName(): ValidatorFn { |
|||
return (c: FormControl) => { |
|||
const newFilter = c.value; |
|||
const found = this.filters.find((filter) => filter.filter === newFilter); |
|||
if (found) { |
|||
if (this.isAdd || this.filter.id !== found.id) { |
|||
return { |
|||
duplicateFilterName: { |
|||
valid: false |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
return null; |
|||
}; |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = !!(control && control.invalid && this.submitted); |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
this.filter.filter = this.filterFormGroup.get('filter').value; |
|||
this.filter.keyFilters = this.filterFormGroup.get('keyFilters').value; |
|||
if (this.isAdd) { |
|||
this.filter.id = this.utils.guid(); |
|||
} |
|||
this.dialogRef.close(this.filter); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<section fxLayout="column" [formGroup]="filterListFormGroup"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;" |
|||
formArrayName="predicates" |
|||
*ngFor="let predicateControl of predicatesFormArray().controls; let $index = index"> |
|||
<tb-filter-predicate |
|||
[userMode]="userMode" |
|||
[valueType]="valueType" |
|||
[formControl]="predicateControl"> |
|||
</tb-filter-predicate> |
|||
<button mat-icon-button color="primary" |
|||
[fxShow]="!disabled && !userMode" |
|||
type="button" |
|||
(click)="removePredicate($index)" |
|||
matTooltip="{{ 'filter.remove-filter' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
<span [fxShow]="!predicatesFormArray().length" |
|||
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}" |
|||
class="no-data-found" translate>filter.no-filters</span> |
|||
<div style="margin-top: 8px;" fxLayout="row" fxLayoutGap="8px"> |
|||
<button mat-button mat-raised-button color="primary" |
|||
[fxShow]="!disabled && !userMode" |
|||
(click)="addPredicate(false)" |
|||
type="button" |
|||
matTooltip="{{ 'filter.add-filter' | translate }}" |
|||
matTooltipPosition="above"> |
|||
{{ 'action.add' | translate }} |
|||
</button> |
|||
<button mat-button mat-raised-button color="primary" |
|||
[fxShow]="!disabled && !userMode" |
|||
(click)="addPredicate(true)" |
|||
type="button" |
|||
matTooltip="{{ 'filter.add-complex-filter' | translate }}" |
|||
matTooltipPosition="above"> |
|||
{{ 'filter.add-complex' | translate }} |
|||
</button> |
|||
</div> |
|||
</section> |
|||
@ -0,0 +1,159 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
ControlValueAccessor, |
|||
FormArray, |
|||
FormBuilder, |
|||
FormGroup, |
|||
NG_VALUE_ACCESSOR, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { Observable, of, Subscription } from 'rxjs'; |
|||
import { |
|||
ComplexFilterPredicate, |
|||
createDefaultFilterPredicate, |
|||
EntityKeyValueType, |
|||
KeyFilterPredicate |
|||
} from '@shared/models/query/query.models'; |
|||
import { |
|||
ComplexFilterPredicateDialogComponent, |
|||
ComplexFilterPredicateDialogData |
|||
} from '@home/components/filter/complex-filter-predicate-dialog.component'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-filter-predicate-list', |
|||
templateUrl: './filter-predicate-list.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => FilterPredicateListComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class FilterPredicateListComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() userMode: boolean; |
|||
|
|||
@Input() valueType: EntityKeyValueType; |
|||
|
|||
filterListFormGroup: FormGroup; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
private valueChangeSubscription: Subscription = null; |
|||
|
|||
constructor(private fb: FormBuilder, |
|||
private dialog: MatDialog) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.filterListFormGroup = this.fb.group({}); |
|||
this.filterListFormGroup.addControl('predicates', |
|||
this.fb.array([])); |
|||
} |
|||
|
|||
predicatesFormArray(): FormArray { |
|||
return this.filterListFormGroup.get('predicates') as FormArray; |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.filterListFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.filterListFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(predicates: Array<KeyFilterPredicate>): void { |
|||
if (this.valueChangeSubscription) { |
|||
this.valueChangeSubscription.unsubscribe(); |
|||
} |
|||
const predicateControls: Array<AbstractControl> = []; |
|||
if (predicates) { |
|||
for (const predicate of predicates) { |
|||
predicateControls.push(this.fb.control(predicate, [Validators.required])); |
|||
} |
|||
} |
|||
this.filterListFormGroup.setControl('predicates', this.fb.array(predicateControls)); |
|||
this.valueChangeSubscription = this.filterListFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
if (this.disabled) { |
|||
this.filterListFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.filterListFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
public removePredicate(index: number) { |
|||
(this.filterListFormGroup.get('predicates') as FormArray).removeAt(index); |
|||
} |
|||
|
|||
public addPredicate(complex: boolean) { |
|||
const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray; |
|||
const predicate = createDefaultFilterPredicate(this.valueType, complex); |
|||
let observable: Observable<KeyFilterPredicate>; |
|||
if (complex) { |
|||
observable = this.openComplexFilterDialog(predicate as ComplexFilterPredicate); |
|||
} else { |
|||
observable = of(predicate); |
|||
} |
|||
observable.subscribe((result) => { |
|||
if (result) { |
|||
predicatesFormArray.push(this.fb.control(result, [Validators.required])); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private openComplexFilterDialog(predicate: ComplexFilterPredicate): Observable<KeyFilterPredicate> { |
|||
return this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData, |
|||
ComplexFilterPredicate>(ComplexFilterPredicateDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
complexPredicate: predicate, |
|||
disabled: this.disabled, |
|||
userMode: this.userMode, |
|||
valueType: this.valueType |
|||
} |
|||
}).afterClosed(); |
|||
} |
|||
|
|||
private updateModel() { |
|||
const predicates: Array<KeyFilterPredicate> = this.filterListFormGroup.getRawValue().predicates; |
|||
if (predicates.length) { |
|||
this.propagateChange(predicates); |
|||
} else { |
|||
this.propagateChange(null); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div fxLayout="column" [formGroup]="filterPredicateFormGroup" |
|||
[ngSwitch]="type"> |
|||
<ng-template [ngSwitchCase]="filterPredicateType.STRING"> |
|||
<tb-string-filter-predicate [userMode]="userMode" |
|||
formControlName="predicate"> |
|||
</tb-string-filter-predicate> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="filterPredicateType.NUMERIC"> |
|||
<tb-numeric-filter-predicate [userMode]="userMode" |
|||
formControlName="predicate"> |
|||
</tb-numeric-filter-predicate> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN"> |
|||
<tb-boolean-filter-predicate [userMode]="userMode" |
|||
formControlName="predicate"> |
|||
</tb-boolean-filter-predicate> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="filterPredicateType.COMPLEX"> |
|||
<tb-complex-filter-predicate |
|||
[valueType]="valueType" |
|||
[userMode]="userMode" |
|||
formControlName="predicate"> |
|||
</tb-complex-filter-predicate> |
|||
</ng-template> |
|||
</div> |
|||
@ -0,0 +1,93 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import { |
|||
EntityKeyValueType, |
|||
FilterPredicateType, KeyFilterPredicate |
|||
} from '@shared/models/query/query.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-filter-predicate', |
|||
templateUrl: './filter-predicate.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => FilterPredicateComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class FilterPredicateComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() userMode: boolean; |
|||
|
|||
@Input() valueType: EntityKeyValueType; |
|||
|
|||
filterPredicateFormGroup: FormGroup; |
|||
|
|||
type: FilterPredicateType; |
|||
|
|||
filterPredicateType = FilterPredicateType; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.filterPredicateFormGroup = this.fb.group({ |
|||
predicate: [null, [Validators.required]] |
|||
}); |
|||
this.filterPredicateFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.filterPredicateFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.filterPredicateFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(predicate: KeyFilterPredicate): void { |
|||
this.type = predicate.type; |
|||
this.filterPredicateFormGroup.get('predicate').patchValue(predicate, {emitEvent: false}); |
|||
} |
|||
|
|||
private updateModel() { |
|||
let predicate: KeyFilterPredicate = null; |
|||
if (this.filterPredicateFormGroup.valid) { |
|||
predicate = this.filterPredicateFormGroup.getRawValue().predicate; |
|||
} |
|||
this.propagateChange(predicate); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-form-field [floatLabel]="showLabel ? 'auto' : 'always'" |
|||
[hideRequiredMarker]="!showLabel" [formGroup]="selectFilterFormGroup" class="mat-block"> |
|||
<mat-label *ngIf="!showLabel"></mat-label> |
|||
<input matInput type="text" placeholder="{{ 'filter.filter' | translate }}" |
|||
#filterInput |
|||
formControlName="filter" |
|||
(focusin)="onFocus()" |
|||
[required]="tbRequired" |
|||
(keydown)="filterEnter($event)" |
|||
(keypress)="filterEnter($event)" |
|||
[matAutocomplete]="filterAutocomplete"> |
|||
<button *ngIf="selectFilterFormGroup.get('filter').value && !disabled" |
|||
type="button" |
|||
matSuffix mat-icon-button aria-label="Clear" |
|||
(click)="clear()"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
<mat-autocomplete class="tb-autocomplete" |
|||
#filterAutocomplete="matAutocomplete" |
|||
[displayWith]="displayFilterFn"> |
|||
<mat-option *ngFor="let filter of filteredFilters | async" [value]="filter"> |
|||
<span [innerHTML]="filter.filter | highlight:searchText"></span> |
|||
</mat-option> |
|||
<mat-option *ngIf="!(filteredFilters | async)?.length" [value]="null" class="tb-not-found"> |
|||
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> |
|||
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty"> |
|||
<span translate>filter.no-filters-found</span> |
|||
</div> |
|||
<ng-template #searchNotEmpty> |
|||
<span> |
|||
{{ translate.get('filter.no-filter-matching', |
|||
{filter: truncate.transform(searchText, true, 6, '...')}) | async }} |
|||
</span> |
|||
</ng-template> |
|||
<span> |
|||
<a translate (click)="createFilter($event, searchText)">filter.create-new-filter</a> |
|||
</span> |
|||
</div> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-error *ngIf="!modelValue && tbRequired"> |
|||
{{ 'filter.filter-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
@ -0,0 +1,22 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Observable } from 'rxjs'; |
|||
import { Filter } from '@shared/models/query/query.models'; |
|||
|
|||
export interface FilterSelectCallbacks { |
|||
createFilter: (filter: string) => Observable<Filter>; |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
|
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-form-field-infix { |
|||
border-top: none; |
|||
} |
|||
} |
|||
@ -0,0 +1,249 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
FormGroupDirective, |
|||
NG_VALUE_ACCESSOR, |
|||
NgForm |
|||
} from '@angular/forms'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { map, mergeMap, share, tap } from 'rxjs/operators'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@app/core/core.state'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
|||
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; |
|||
import { ENTER } from '@angular/cdk/keycodes'; |
|||
import { ErrorStateMatcher } from '@angular/material/core'; |
|||
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; |
|||
import { Filter } from '@shared/models/query/query.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-filter-select', |
|||
templateUrl: './filter-select.component.html', |
|||
styleUrls: ['./filter-select.component.scss'], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => FilterSelectComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: ErrorStateMatcher, |
|||
useExisting: FilterSelectComponent |
|||
}] |
|||
}) |
|||
export class FilterSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { |
|||
|
|||
selectFilterFormGroup: FormGroup; |
|||
|
|||
modelValue: string | null; |
|||
|
|||
@Input() |
|||
aliasController: IAliasController; |
|||
|
|||
@Input() |
|||
callbacks: FilterSelectCallbacks; |
|||
|
|||
@Input() |
|||
showLabel: boolean; |
|||
|
|||
@ViewChild('filterAutocomplete') filterAutocomplete: MatAutocomplete; |
|||
@ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger; |
|||
|
|||
|
|||
private requiredValue: boolean; |
|||
get tbRequired(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set tbRequired(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@ViewChild('filterInput', {static: true}) filterInput: ElementRef; |
|||
|
|||
filterList: Array<Filter> = []; |
|||
|
|||
filteredFilters: Observable<Array<Filter>>; |
|||
|
|||
searchText = ''; |
|||
|
|||
private dirty = false; |
|||
|
|||
private creatingFilter = false; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public translate: TranslateService, |
|||
public truncate: TruncatePipe, |
|||
private fb: FormBuilder) { |
|||
this.selectFilterFormGroup = this.fb.group({ |
|||
filter: [null] |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
const filters = this.aliasController.getFilters(); |
|||
for (const filterId of Object.keys(filters)) { |
|||
this.filterList.push(filters[filterId]); |
|||
} |
|||
|
|||
this.filteredFilters = this.selectFilterFormGroup.get('filter').valueChanges |
|||
.pipe( |
|||
tap(value => { |
|||
let modelValue; |
|||
if (typeof value === 'string' || !value) { |
|||
modelValue = null; |
|||
} else { |
|||
modelValue = value; |
|||
} |
|||
this.updateView(modelValue); |
|||
if (value === null) { |
|||
this.clear(); |
|||
} |
|||
}), |
|||
map(value => value ? (typeof value === 'string' ? value : value.filter) : ''), |
|||
mergeMap(name => this.fetchFilters(name) ), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = this.tbRequired && !this.modelValue; |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
ngAfterViewInit(): void {} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.selectFilterFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.selectFilterFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: string | null): void { |
|||
this.searchText = ''; |
|||
let filter = null; |
|||
if (value != null) { |
|||
const filters = this.aliasController.getFilters(); |
|||
if (filters[value]) { |
|||
filter = filters[value]; |
|||
} |
|||
} |
|||
if (filter != null) { |
|||
this.modelValue = filter.id; |
|||
this.selectFilterFormGroup.get('filter').patchValue(filter, {emitEvent: false}); |
|||
} else { |
|||
this.modelValue = null; |
|||
this.selectFilterFormGroup.get('filter').patchValue('', {emitEvent: false}); |
|||
} |
|||
this.dirty = true; |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.selectFilterFormGroup.get('filter').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
updateView(value: Filter | null) { |
|||
const filterId = value ? value.id : null; |
|||
if (this.modelValue !== filterId) { |
|||
this.modelValue = filterId; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
|||
displayFilterFn(filter?: Filter): string | undefined { |
|||
return filter ? filter.filter : undefined; |
|||
} |
|||
|
|||
fetchFilters(searchText?: string): Observable<Array<Filter>> { |
|||
this.searchText = searchText; |
|||
let result = this.filterList; |
|||
if (searchText && searchText.length) { |
|||
result = this.filterList.filter((filter) => filter.filter.toLowerCase().includes(searchText.toLowerCase())); |
|||
} |
|||
return of(result); |
|||
} |
|||
|
|||
clear(value: string = '') { |
|||
this.filterInput.nativeElement.value = value; |
|||
this.selectFilterFormGroup.get('filter').patchValue(value, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.filterInput.nativeElement.blur(); |
|||
this.filterInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
textIsNotEmpty(text: string): boolean { |
|||
return (text && text != null && text.length > 0) ? true : false; |
|||
} |
|||
|
|||
filterEnter($event: KeyboardEvent) { |
|||
if ($event.keyCode === ENTER) { |
|||
$event.preventDefault(); |
|||
if (!this.modelValue) { |
|||
this.createFilter($event, this.searchText); |
|||
} |
|||
} |
|||
} |
|||
|
|||
createFilter($event: Event, filter: string) { |
|||
$event.preventDefault(); |
|||
this.creatingFilter = true; |
|||
if (this.callbacks && this.callbacks.createFilter) { |
|||
this.callbacks.createFilter(filter).subscribe((newFilter) => { |
|||
if (!newFilter) { |
|||
setTimeout(() => { |
|||
this.filterInput.nativeElement.blur(); |
|||
this.filterInput.nativeElement.focus(); |
|||
}, 0); |
|||
} else { |
|||
this.filterList.push(newFilter); |
|||
this.modelValue = newFilter.id; |
|||
this.selectFilterFormGroup.get('filter').patchValue(newFilter, {emitEvent: true}); |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form [formGroup]="filtersFormGroup" (ngSubmit)="save()" style="width: 700px;"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ title | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content> |
|||
<div class="tb-filters-header" fxLayout="row" fxLayoutAlign="start center"> |
|||
<span fxFlex="5"></span> |
|||
<div fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> |
|||
<span class="tb-header-label" translate fxFlex>filter.filter</span> |
|||
<span style="min-width: 80px;"></span> |
|||
</div> |
|||
</div> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<mat-divider></mat-divider> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" |
|||
formArrayName="filters" |
|||
*ngFor="let filterControl of filtersFormArray().controls; let $index = index"> |
|||
<span fxFlex="5">{{$index + 1}}.</span> |
|||
<div class="mat-elevation-z4 tb-filter" fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> |
|||
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block" fxFlex> |
|||
<mat-label></mat-label> |
|||
<input matInput [formControl]="filterControl.get('filter')" required placeholder="{{ 'filter.filter' | translate }}"> |
|||
<mat-error *ngIf="filterControl.get('filter').hasError('required')"> |
|||
{{ 'filter.filter-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<button [disabled]="isLoading$ | async" |
|||
mat-icon-button color="primary" |
|||
style="min-width: 40px;" |
|||
type="button" |
|||
(click)="editFilter($index)" |
|||
matTooltip="{{ 'filter.edit' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>edit</mat-icon> |
|||
</button> |
|||
<button [disabled]="isLoading$ | async" |
|||
mat-icon-button color="primary" |
|||
style="min-width: 40px;" |
|||
type="button" |
|||
(click)="removeFilter($index)" |
|||
matTooltip="{{ 'filter.remove-filter' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions> |
|||
<button mat-raised-button color="primary" |
|||
type="button" |
|||
(click)="addFilter()" |
|||
[fxShow]="!disableAdd" |
|||
[disabled]="isLoading$ | async" |
|||
matTooltip="{{ 'filter.add' | translate }}" |
|||
matTooltipPosition="above"> |
|||
{{ 'filter.add' | translate }} |
|||
</button> |
|||
<span fxFlex></span> |
|||
<button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || filtersFormGroup.invalid || !filtersFormGroup.dirty"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" |
|||
cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,44 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
|
|||
.tb-filters-header { |
|||
min-height: 40px; |
|||
padding: 0 11px; |
|||
margin: 5px; |
|||
|
|||
.tb-header-label { |
|||
font-size: 14px; |
|||
color: rgba(0, 0, 0, .570588); |
|||
} |
|||
} |
|||
|
|||
mat-divider{ |
|||
margin: -1px -24px; |
|||
} |
|||
|
|||
.tb-filter { |
|||
padding: 0 0 0 10px; |
|||
margin: 5px; |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-dialog-content { |
|||
padding-top: 0 !important; |
|||
padding-bottom: 0 !important; |
|||
} |
|||
} |
|||
@ -0,0 +1,241 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; |
|||
import { ErrorStateMatcher } from '@angular/material/core'; |
|||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { |
|||
AbstractControl, |
|||
FormArray, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
FormGroupDirective, |
|||
NgForm, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { DatasourceType, Widget } from '@shared/models/widget.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { Filter, Filters, KeyFilterInfo } from '@shared/models/query/query.models'; |
|||
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; |
|||
|
|||
export interface FiltersDialogData { |
|||
filters: Filters; |
|||
widgets: Array<Widget>; |
|||
isSingleFilter?: boolean; |
|||
isSingleWidget?: boolean; |
|||
disableAdd?: boolean; |
|||
singleFilter?: Filter; |
|||
customTitle?: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-filters-dialog', |
|||
templateUrl: './filters-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: FiltersDialogComponent}], |
|||
styleUrls: ['./filters-dialog.component.scss'] |
|||
}) |
|||
export class FiltersDialogComponent extends DialogComponent<FiltersDialogComponent, Filters> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
title: string; |
|||
disableAdd: boolean; |
|||
|
|||
filterToWidgetsMap: {[filterId: string]: Array<string>} = {}; |
|||
|
|||
filtersFormGroup: FormGroup; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: FiltersDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<FiltersDialogComponent, Filters>, |
|||
private fb: FormBuilder, |
|||
private utils: UtilsService, |
|||
private translate: TranslateService, |
|||
private dialogs: DialogService, |
|||
private dialog: MatDialog) { |
|||
super(store, router, dialogRef); |
|||
this.title = data.customTitle ? data.customTitle : 'filter.filters'; |
|||
this.disableAdd = this.data.disableAdd; |
|||
|
|||
if (data.widgets) { |
|||
let widgetsTitleList: Array<string>; |
|||
if (this.data.isSingleWidget && this.data.widgets.length === 1) { |
|||
const widget = this.data.widgets[0]; |
|||
widgetsTitleList = [widget.config.title]; |
|||
for (const filterId of Object.keys(this.data.filters)) { |
|||
this.filterToWidgetsMap[filterId] = widgetsTitleList; |
|||
} |
|||
} else { |
|||
this.data.widgets.forEach((widget) => { |
|||
const datasources = this.utils.validateDatasources(widget.config.datasources); |
|||
datasources.forEach((datasource) => { |
|||
if (datasource.type === DatasourceType.entity && datasource.filterId) { |
|||
widgetsTitleList = this.filterToWidgetsMap[datasource.filterId]; |
|||
if (!widgetsTitleList) { |
|||
widgetsTitleList = []; |
|||
this.filterToWidgetsMap[datasource.filterId] = widgetsTitleList; |
|||
} |
|||
widgetsTitleList.push(widget.config.title); |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
const filterControls: Array<AbstractControl> = []; |
|||
for (const filterId of Object.keys(this.data.filters)) { |
|||
const filter = this.data.filters[filterId]; |
|||
filterControls.push(this.createFilterFormControl(filterId, filter)); |
|||
} |
|||
|
|||
this.filtersFormGroup = this.fb.group({ |
|||
filters: this.fb.array(filterControls) |
|||
}); |
|||
} |
|||
|
|||
private createFilterFormControl(filterId: string, filter: Filter): AbstractControl { |
|||
const filterFormControl = this.fb.group({ |
|||
id: [filterId], |
|||
filter: [filter ? filter.filter : null, [Validators.required]], |
|||
keyFilters: [filter ? filter.keyFilters : [], [Validators.required]] |
|||
}); |
|||
return filterFormControl; |
|||
} |
|||
|
|||
|
|||
filtersFormArray(): FormArray { |
|||
return this.filtersFormGroup.get('filters') as FormArray; |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = !!(control && control.invalid && this.submitted); |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
removeFilter(index: number) { |
|||
const filter = (this.filtersFormGroup.get('filters').value as any[])[index]; |
|||
const widgetsTitleList = this.filterToWidgetsMap[filter.id]; |
|||
if (widgetsTitleList) { |
|||
let widgetsListHtml = ''; |
|||
for (const widgetTitle of widgetsTitleList) { |
|||
widgetsListHtml += '<br/>\'' + widgetTitle + '\''; |
|||
} |
|||
const message = this.translate.instant('entity.unable-delete-filter-text', |
|||
{filter: filter.filter, widgetsList: widgetsListHtml}); |
|||
this.dialogs.alert(this.translate.instant('entity.unable-delete-filter-title'), |
|||
message, this.translate.instant('action.close'), true); |
|||
} else { |
|||
(this.filtersFormGroup.get('filters') as FormArray).removeAt(index); |
|||
this.filtersFormGroup.markAsDirty(); |
|||
} |
|||
} |
|||
|
|||
public addFilter() { |
|||
this.openFilterDialog(-1); |
|||
} |
|||
|
|||
public editFilter(index: number) { |
|||
this.openFilterDialog(index); |
|||
} |
|||
|
|||
private openFilterDialog(index: number) { |
|||
const isAdd = index === -1; |
|||
let filter; |
|||
const filtersArray = this.filtersFormGroup.get('filters').value as any[]; |
|||
if (!isAdd) { |
|||
filter = filtersArray[index]; |
|||
} |
|||
this.dialog.open<FilterDialogComponent, FilterDialogData, |
|||
Filter>(FilterDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
isAdd, |
|||
filters: filtersArray, |
|||
filter: isAdd ? null : deepClone(filter), |
|||
userMode: false |
|||
} |
|||
}).afterClosed().subscribe((result) => { |
|||
if (result) { |
|||
if (isAdd) { |
|||
(this.filtersFormGroup.get('filters') as FormArray) |
|||
.push(this.createFilterFormControl(result.id, result)); |
|||
} else { |
|||
const filterFormControl = (this.filtersFormGroup.get('filters') as FormArray).at(index); |
|||
filterFormControl.get('filter').patchValue(filter.filter); |
|||
filterFormControl.get('keyFilters').patchValue(filter.keyFilters); |
|||
} |
|||
this.filtersFormGroup.markAsDirty(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
const filters: Filters = {}; |
|||
const uniqueFilterList: {[filter: string]: string} = {}; |
|||
|
|||
let valid = true; |
|||
let message: string; |
|||
|
|||
const filtersArray = this.filtersFormGroup.get('filters').value as any[]; |
|||
for (const filterValue of filtersArray) { |
|||
const filterId: string = filterValue.id; |
|||
const filter: string = filterValue.filter; |
|||
const keyFilters: Array<KeyFilterInfo> = filterValue.keyFilters; |
|||
if (uniqueFilterList[filter]) { |
|||
valid = false; |
|||
message = this.translate.instant('filter.duplicate-filter-error', {filter}); |
|||
break; |
|||
} else if (!keyFilters || !keyFilters.length) { |
|||
valid = false; |
|||
message = this.translate.instant('filter.missing-key-filters-error', {filter}); |
|||
break; |
|||
} else { |
|||
uniqueFilterList[filter] = filter; |
|||
filters[filterId] = {id: filterId, filter, keyFilters}; |
|||
} |
|||
} |
|||
if (valid) { |
|||
this.dialogRef.close(filters); |
|||
} else { |
|||
this.store.dispatch(new ActionNotificationShow( |
|||
{ |
|||
message, |
|||
type: 'error' |
|||
})); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()"> |
|||
<mat-toolbar color="primary"> |
|||
<h2 translate>{{data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter'}}</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async" fxLayout="column"> |
|||
<section fxLayout="row" fxLayoutGap="8px"> |
|||
<section fxFlex="60" fxLayout="row" formGroupName="key" fxLayoutGap="8px"> |
|||
<mat-form-field fxFlex="40" class="mat-block"> |
|||
<mat-label translate>filter.key-name</mat-label> |
|||
<input matInput required formControlName="key"> |
|||
<mat-error *ngIf="keyFilterFormGroup.get('key.key').hasError('required')"> |
|||
{{ 'filter.key-name-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="60" class="mat-block"> |
|||
<mat-label translate>filter.key-type.key-type</mat-label> |
|||
<mat-select required formControlName="type"> |
|||
<mat-option *ngFor="let type of entityKeyTypes" [value]="type"> |
|||
{{entityKeyTypeTranslations.get(type) | translate}} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</section> |
|||
<mat-form-field fxFlex="40" class="mat-block"> |
|||
<mat-label translate>filter.value-type.value-type</mat-label> |
|||
<mat-select matInput formControlName="valueType"> |
|||
<mat-select-trigger> |
|||
<mat-icon svgIcon="{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.icon }}"></mat-icon> |
|||
<span>{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }}</span> |
|||
</mat-select-trigger> |
|||
<mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType"> |
|||
<mat-icon svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon> |
|||
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span> |
|||
</mat-option> |
|||
</mat-select> |
|||
<mat-error *ngIf="keyFilterFormGroup.get('valueType').hasError('required')"> |
|||
{{ 'filter.value-type-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</section> |
|||
<tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value" |
|||
[userMode]="data.userMode" |
|||
[valueType]="keyFilterFormGroup.get('valueType').value" |
|||
formControlName="predicates"> |
|||
</tb-filter-predicate-list> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid"> |
|||
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" |
|||
cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,105 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; |
|||
import { ErrorStateMatcher } from '@angular/material/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { |
|||
EntityKeyType, |
|||
entityKeyTypeTranslationMap, |
|||
EntityKeyValueType, |
|||
entityKeyValueTypesMap, |
|||
KeyFilterInfo |
|||
} from '@shared/models/query/query.models'; |
|||
|
|||
export interface KeyFilterDialogData { |
|||
keyFilter: KeyFilterInfo; |
|||
userMode: boolean; |
|||
isAdd: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-key-filter-dialog', |
|||
templateUrl: './key-filter-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: KeyFilterDialogComponent}], |
|||
styleUrls: [] |
|||
}) |
|||
export class KeyFilterDialogComponent extends |
|||
DialogComponent<KeyFilterDialogComponent, KeyFilterInfo> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
keyFilterFormGroup: FormGroup; |
|||
|
|||
entityKeyTypes = [EntityKeyType.ENTITY_FIELD, EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES]; |
|||
|
|||
entityKeyTypeTranslations = entityKeyTypeTranslationMap; |
|||
|
|||
entityKeyValueTypesKeys = Object.keys(EntityKeyValueType); |
|||
|
|||
entityKeyValueTypeEnum = EntityKeyValueType; |
|||
|
|||
entityKeyValueTypes = entityKeyValueTypesMap; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<KeyFilterDialogComponent, KeyFilterInfo>, |
|||
private fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.keyFilterFormGroup = this.fb.group( |
|||
{ |
|||
key: this.fb.group( |
|||
{ |
|||
type: [this.data.keyFilter.key.type, [Validators.required]], |
|||
key: [this.data.keyFilter.key.key, [Validators.required]] |
|||
} |
|||
), |
|||
valueType: [this.data.keyFilter.valueType, [Validators.required]], |
|||
predicates: [this.data.keyFilter.predicates, [Validators.required]] |
|||
} |
|||
); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = !!(control && control.invalid && this.submitted); |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
if (this.keyFilterFormGroup.valid) { |
|||
const keyFilter: KeyFilterInfo = this.keyFilterFormGroup.getRawValue(); |
|||
this.dialogRef.close(keyFilter); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<section fxLayout="column" [formGroup]="keyFilterListFormGroup"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;" |
|||
formArrayName="keyFilters" |
|||
*ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index"> |
|||
<span>{{ keyFilterControl.value.key.key }}</span> |
|||
<span>{{ keyFilterControl.value.key.type }}</span> |
|||
<button mat-icon-button color="primary" |
|||
type="button" |
|||
(click)="editKeyFilter($index)" |
|||
matTooltip="{{ 'filter.edit-key-filter' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>edit</mat-icon> |
|||
</button> |
|||
<button mat-icon-button color="primary" |
|||
[fxShow]="!disabled && !userMode" |
|||
type="button" |
|||
(click)="removeKeyFilter($index)" |
|||
matTooltip="{{ 'filter.remove-key-filter' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
<span [fxShow]="!keyFiltersFormArray().length" |
|||
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}" |
|||
class="no-data-found" translate>filter.no-key-filters</span> |
|||
<div style="margin-top: 8px;"> |
|||
<button mat-button mat-raised-button color="primary" |
|||
[fxShow]="!disabled && !userMode" |
|||
(click)="addKeyFilter()" |
|||
type="button" |
|||
matTooltip="{{ 'filter.add-key-filter' | translate }}" |
|||
matTooltipPosition="above"> |
|||
{{ 'filter.add-key-filter' | translate }} |
|||
</button> |
|||
</div> |
|||
</section> |
|||
@ -0,0 +1,165 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
ControlValueAccessor, |
|||
FormArray, |
|||
FormBuilder, |
|||
FormGroup, |
|||
NG_VALUE_ACCESSOR, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { Observable, Subscription } from 'rxjs'; |
|||
import { EntityKeyType, KeyFilterInfo } from '@shared/models/query/query.models'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-key-filter-list', |
|||
templateUrl: './key-filter-list.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => KeyFilterListComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class KeyFilterListComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() userMode: boolean; |
|||
|
|||
keyFilterListFormGroup: FormGroup; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
private valueChangeSubscription: Subscription = null; |
|||
|
|||
constructor(private fb: FormBuilder, |
|||
private dialog: MatDialog) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.keyFilterListFormGroup = this.fb.group({}); |
|||
this.keyFilterListFormGroup.addControl('keyFilters', |
|||
this.fb.array([])); |
|||
} |
|||
|
|||
keyFiltersFormArray(): FormArray { |
|||
return this.keyFilterListFormGroup.get('keyFilters') as FormArray; |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.keyFilterListFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.keyFilterListFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(keyFilters: Array<KeyFilterInfo>): void { |
|||
if (this.valueChangeSubscription) { |
|||
this.valueChangeSubscription.unsubscribe(); |
|||
} |
|||
const keyFilterControls: Array<AbstractControl> = []; |
|||
if (keyFilters) { |
|||
for (const keyFilter of keyFilters) { |
|||
keyFilterControls.push(this.fb.control(keyFilter, [Validators.required])); |
|||
} |
|||
} |
|||
this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls)); |
|||
this.valueChangeSubscription = this.keyFilterListFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
if (this.disabled) { |
|||
this.keyFilterListFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.keyFilterListFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
public removeKeyFilter(index: number) { |
|||
(this.keyFilterListFormGroup.get('keyFilters') as FormArray).removeAt(index); |
|||
} |
|||
|
|||
public addKeyFilter() { |
|||
const keyFiltersFormArray = this.keyFilterListFormGroup.get('keyFilters') as FormArray; |
|||
this.openKeyFilterDialog(null).subscribe((result) => { |
|||
if (result) { |
|||
keyFiltersFormArray.push(this.fb.control(result, [Validators.required])); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public editKeyFilter(index: number) { |
|||
const keyFilter: KeyFilterInfo = |
|||
(this.keyFilterListFormGroup.get('keyFilters') as FormArray).at(index).value; |
|||
this.openKeyFilterDialog(keyFilter).subscribe( |
|||
(result) => { |
|||
if (result) { |
|||
(this.keyFilterListFormGroup.get('keyFilters') as FormArray).at(index).patchValue(result); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
private openKeyFilterDialog(keyFilter?: KeyFilterInfo): Observable<KeyFilterInfo> { |
|||
const isAdd = !keyFilter; |
|||
if (!keyFilter) { |
|||
keyFilter = { |
|||
key: { |
|||
key: '', |
|||
type: EntityKeyType.ATTRIBUTE |
|||
}, |
|||
valueType: null, |
|||
predicates: [] |
|||
}; |
|||
} |
|||
return this.dialog.open<KeyFilterDialogComponent, KeyFilterDialogData, |
|||
KeyFilterInfo>(KeyFilterDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
keyFilter: keyFilter ? deepClone(keyFilter): null, |
|||
userMode: this.userMode, |
|||
isAdd |
|||
} |
|||
}).afterClosed(); |
|||
} |
|||
|
|||
private updateModel() { |
|||
const keyFilters: Array<KeyFilterInfo> = this.keyFilterListFormGroup.getRawValue().keyFilters; |
|||
if (keyFilters.length) { |
|||
this.propagateChange(keyFilters); |
|||
} else { |
|||
this.propagateChange(null); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div fxLayout="row" [formGroup]="numericFilterPredicateFormGroup"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>filter.operation.operation</mat-label> |
|||
<mat-select required formControlName="operation"> |
|||
<mat-option *ngFor="let operation of numericOperations" [value]="operation"> |
|||
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>filter.value</mat-label> |
|||
<input required type="number" matInput formControlName="value"> |
|||
</mat-form-field> |
|||
</div> |
|||
@ -0,0 +1,99 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import { |
|||
FilterPredicateType, NumericFilterPredicate, NumericOperation, numericOperationTranslationMap, |
|||
} from '@shared/models/query/query.models'; |
|||
import { isDefined } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-numeric-filter-predicate', |
|||
templateUrl: './numeric-filter-predicate.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => NumericFilterPredicateComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class NumericFilterPredicateComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() userMode: boolean; |
|||
|
|||
numericFilterPredicateFormGroup: FormGroup; |
|||
|
|||
numericOperations = Object.keys(NumericOperation); |
|||
numericOperationEnum = NumericOperation; |
|||
numericOperationTranslations = numericOperationTranslationMap; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.numericFilterPredicateFormGroup = this.fb.group({ |
|||
operation: [NumericOperation.EQUAL, [Validators.required]], |
|||
value: [0, [Validators.required]] |
|||
}); |
|||
if (this.userMode) { |
|||
this.numericFilterPredicateFormGroup.get('operation').disable({emitEvent: false}); |
|||
} |
|||
this.numericFilterPredicateFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.numericFilterPredicateFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.numericFilterPredicateFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(predicate: NumericFilterPredicate): void { |
|||
this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); |
|||
this.numericFilterPredicateFormGroup.get('value').patchValue(isDefined(predicate.value) ? predicate.value : 0, {emitEvent: false}); |
|||
} |
|||
|
|||
private updateModel() { |
|||
let predicate: NumericFilterPredicate = null; |
|||
if (this.numericFilterPredicateFormGroup.valid) { |
|||
predicate = this.numericFilterPredicateFormGroup.getRawValue(); |
|||
if (!predicate.value) { |
|||
predicate.value = 0; |
|||
} |
|||
predicate.type = FilterPredicateType.NUMERIC; |
|||
} |
|||
this.propagateChange(predicate); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div fxLayout="row" [formGroup]="stringFilterPredicateFormGroup"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>filter.operation.operation</mat-label> |
|||
<mat-select required formControlName="operation"> |
|||
<mat-option *ngFor="let operation of stringOperations" [value]="operation"> |
|||
{{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-checkbox formControlName="ignoreCase"> |
|||
{{ 'filter.ignore-case' | translate }} |
|||
</mat-checkbox> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>filter.value</mat-label> |
|||
<input matInput formControlName="value"> |
|||
</mat-form-field> |
|||
</div> |
|||
@ -0,0 +1,104 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import { |
|||
FilterPredicateType, |
|||
StringFilterPredicate, |
|||
StringOperation, |
|||
stringOperationTranslationMap |
|||
} from '@shared/models/query/query.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-string-filter-predicate', |
|||
templateUrl: './string-filter-predicate.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => StringFilterPredicateComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class StringFilterPredicateComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() userMode: boolean; |
|||
|
|||
stringFilterPredicateFormGroup: FormGroup; |
|||
|
|||
stringOperations = Object.keys(StringOperation); |
|||
stringOperationEnum = StringOperation; |
|||
stringOperationTranslations = stringOperationTranslationMap; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.stringFilterPredicateFormGroup = this.fb.group({ |
|||
operation: [StringOperation.STARTS_WITH, [Validators.required]], |
|||
value: [''], |
|||
ignoreCase: [false] |
|||
}); |
|||
if (this.userMode) { |
|||
this.stringFilterPredicateFormGroup.get('operation').disable({emitEvent: false}); |
|||
this.stringFilterPredicateFormGroup.get('ignoreCase').disable({emitEvent: false}); |
|||
} |
|||
this.stringFilterPredicateFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.stringFilterPredicateFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.stringFilterPredicateFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(predicate: StringFilterPredicate): void { |
|||
this.stringFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); |
|||
this.stringFilterPredicateFormGroup.get('value').patchValue(predicate.value ? predicate.value : '', {emitEvent: false}); |
|||
this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false}); |
|||
} |
|||
|
|||
private updateModel() { |
|||
let predicate: StringFilterPredicate = null; |
|||
if (this.stringFilterPredicateFormGroup.valid) { |
|||
predicate = this.stringFilterPredicateFormGroup.getRawValue(); |
|||
if (!predicate.value) { |
|||
predicate.value = ''; |
|||
} |
|||
predicate.type = FilterPredicateType.STRING; |
|||
} |
|||
this.propagateChange(predicate); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue